-
Notifications
You must be signed in to change notification settings - Fork 687
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Preselect lowest cost shipping method for authed users without one #2402
Conversation
|
Performance Test Results The following fails have been reported by WebpageTest. These numbers indicates a possible performance issue with the PR which requires further manual testing to validate. https://pr-2402.pwa-venia.com : LH Performance Expected 0.85 Actual 0.58, LH Best Practices Expected 1 Actual 0.92, WPT Cache Expected 90 Actual 88 |
visual hiccup and erroneous error messages
const bodyContents = | ||
displayState === displayStates.INITIALIZING | ||
? initializingContents | ||
: editingContents; |
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 only logic that actually changed is the addition of what to do when displayState
is INITIALIZING
. The other changes here are just for readability.
|
||
// If an authenticated user does not have a preferred shipping method, | ||
// auto-select the least expensive one for them. | ||
useEffect(() => { |
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 is the meat of the PR, everything else is minor changes to support a seamless experience for the user.
I should note that the Update Modal also has a new behavior: The call to get all available shipping methods is In the case where the network data actually differs from what is in the cache, the modal still disables the "submit" button until the network responds. In other words, you can't actually make a different selection until we know we have the most up-to-date data, but we no longer show a loading spinner until that happens. This does introduce the potential for the selection list to change while the user is looking at it. |
@supernova-at I followed the steps, and the shipping method behaves as you described. That part is UX approved. The use-case I wanted to test but can't until Tommy's work is merged is that very first time someone comes through auth checkout and doesn't have a saved shipping address. Once they enter the shipping address in the new checkout flow, I assume the shipping method behavior will stay consistent? |
Yes, it should behave as it does 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.
Nice work, all the logic is here and this functions perfectly 👌 I've added some suggestions that should clean up the talon quite a bit, but I'd be fine to save the big changes for another task after we've had a chance to discuss as a team.
const [displayState, setDisplayState] = useState( | ||
displayStates.INITIALIZING | ||
); | ||
const [isBackgroundAutoSelecting, setIsBackgroundAutoSelecting] = useState( |
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.
You shouldn't need this, its just duplicating state you already have access to. You should track called
and loading
state of setShippingMethodCall
, and just have a derived isLoading
that looks at both. Will add a comment by nextStateDisplay
with what I'm talking about.
@@ -159,9 +167,67 @@ export const useShippingMethod = props => { | |||
// Determine the component's display state. | |||
const nextDisplayState = selectedMethod | |||
? displayStates.DONE | |||
: loading || isBackgroundAutoSelecting |
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.
Example of using state you already have, with some renames. In some of my recent code I've found tracking called isn't needed, but it reads better in this example; consider it optional.
: loading || isBackgroundAutoSelecting | |
: fetchShippingMethodLoading || (setShippingMethodCalled && setShippingMethodLoading) |
// Functions passed to useEffect should be synchronous. | ||
// Set this helper function up as async so we can wait on the mutation | ||
// before re-querying. | ||
const autoSelectShippingMethod = async shippingMethod => { |
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.
Shouldn't need this after isBackgroundAutoSelecting
state is removed.
setIsBackgroundAutoSelecting(true); | ||
|
||
// Perform the operation on the backend. | ||
await setShippingMethodCall({ |
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.
Use the same fragment as fetchShippingMethodInfo
and you won't need to do your re-fetch below. This also should mean you can worry less about the execution flow.
If you absolutely must refetch this way, you can still do that without this async function. Use the refetchQueries
option of the mutation.
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.
Tommy is right - if the data can be returned by a mutation and assuming the response data is correct (as in no bug in GQL) we should opt to query for that data in the response rather than firing a second request/query. If you're able to get all you need in the response you can get rid of the extra loading state variable too. Seems like a win-win!
Edit: I'll add that we did see that loading
prop returned from the Apollo hooks does not reflect state of in-flight refetchQueries
.
}); | ||
}; | ||
|
||
const primaryAddress = data.cart.shipping_addresses[0]; |
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 should be unreachable without an address set, but still good to guard against shipping_addresses
being null
.
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.
Should be a possibility once virtual products are supported.
const leastExpensiveShippingMethod = shippingMethodsByPrice[0]; | ||
|
||
if (leastExpensiveShippingMethod) { | ||
autoSelectShippingMethod(leastExpensiveShippingMethod); |
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 is where you would move the setShippingMethodCall
.
const [isUpdateMode, setIsUpdateMode] = useState(false); | ||
const [selectedShippingMethod, setSelectedShippingMethod] = useState(null); | ||
const [shippingMethods, setShippingMethods] = useState([]); |
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 know this pattern existing prior to this PR, but wanted to discuss why I think it might be an anti-pattern, and something we should avoid. Apollo's hooks are effects themselves, so you should never need to wrap that returned state in an effect, or duplicate it into its own state. I'm a bit unique in that I didn't write much React before hooks, but I think the pattern you're going for here from previous paradigms is derivedState.
Two use cases for derived state:
- Data needs manipulated -
useMemo
if complex, but option 2 also works, wrap logic in a function:
const memoizedShippingMethods = useMemo(() => {
let mutatedShippingMethods = [];
if (data) {
const primaryShippingAddress = data.cart.shipping_addresses[0];
// Shape the list of available shipping methods.
// Sort them by price and add a serialized property to each.
const rawShippingMethods =
primaryShippingAddress.available_shipping_methods;
const shippingMethodsByPrice = [...rawShippingMethods].sort(
byPrice
);
mutatedShippingMethods = shippingMethodsByPrice.map(
addSerializedProperty
);
}
return mutatedShippingMethods;
}, [data]);
- Data just needs extracted - no special treatment
const derviedSelectedShippingMethod =
data &&
addSerializedProperty(
data.cart.shipping_addresses[0].selected_shipping_method
); // you may want a || null default value here
I would like to earmark this to be discussed during tomorrow's team time, as I think we should make sure we're all aligned on this. I'm not 100% I'm right, so worth discussing for my own benefit as well.
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.
const bodyContents = | ||
displayState === displayStates.INITIALIZING | ||
? initializingContents | ||
: editingContents; |
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.
Recent feedback from Jimmy, very minor. We've already paid the cost of creating the editingContent
element and this means we may never render it. Best to only create elements we intend to render.
? displayStates.INITIALIZING | ||
: displayStates.EDITING; | ||
|
||
if (nextDisplayState !== displayState) { |
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.
You don't need this check; if React detects the new state matches the previous it should skip the update.
@@ -40,7 +40,7 @@ test('it renders correctly', () => { | |||
expect(instance.toJSON()).toMatchSnapshot(); | |||
}); | |||
|
|||
test('it renders correctly during loading', () => { | |||
test('it disables the submit butting while loading', () => { |
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.
Butting whom?
…studio into supernova/573_ship_shortcut
PR updated with derived state! This greatly simplifies the whole talon thank you so much @tjwiebell ! I am also trying to sneak in the disabling of the input radios when the page is updating 😉 . I think Shipping Methods is in a much better place now 💪 |
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.
Re-factor looks amazing, nice work 👍 Definitely copying how you structure your talon sections into my PR, makes it really easy to understand.
const isBackgroundAutoSelecting = | ||
isSignedIn && | ||
!derivedSelectedShippingMethod && | ||
Boolean(derivedShippingMethods.length); |
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.
👌
Could not test edit address flow for Auth users but other than that it look good. |
Description
This PR pre-selects the lowest cost shipping method for authenticated (signed in) users who don't yet have a saved / preferred shipping method.
I also added a new
INITIALIZING
value to thedisplayState
enumeration to remove a visual hiccup that occurred during auto-selection of a shipping method. TheShippingMethod
component should beINITIALIZING
before fetching its data and while the auto-selection is in progress.Adding this
INITIALIZING
state allowed us to greatly simplify downstream logic as well 🎉 .Related Issue
Closes PWA-573.
Acceptance
Verification Stakeholders
@soumya-ashok
Specification
Verification Steps
/checkout
pageScreenshots / Screen Captures (if appropriate)
Checklist