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

Preselect lowest cost shipping method for authed users without one #2402

Merged
merged 22 commits into from
May 26, 2020

Conversation

supernova-at
Copy link
Contributor

@supernova-at supernova-at commented May 18, 2020

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 the displayState enumeration to remove a visual hiccup that occurred during auto-selection of a shipping method. The ShippingMethod component should be INITIALIZING 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

  1. Sign in
  2. Add an item to your cart
  3. Set a shipping address (via mini-cart if [PWA-245] Shipping Information (Authenticated) #2380 isn't merged yet)
  4. Go to the /checkout page
  5. Verify that you're never actually presented with the shipping radios to select; the lowest cost shipping option (FREE) is selected for you (after some loading) and you are taken directly to the payment section
  6. Fill out the payment section
  7. Verify that you can successfully place your order

Screenshots / Screen Captures (if appropriate)

573_update

Checklist

  • I have updated the documentation accordingly, if necessary.
  • I have added tests to cover my changes, if necessary.

@PWAStudioBot
Copy link
Contributor

PWAStudioBot commented May 18, 2020

Messages
📖

Access a deployed version of this PR here. Make sure to wait for the "pwa-pull-request-deploy" job to complete.

📖 DangerCI Failures related to missing labels/description/linked issues/etc will persist until the next push or next nightly build run (assuming they are fixed).
📖

Associated JIRA tickets: PWA-573.

Generated by 🚫 dangerJS against 8ba5c8f

@devops-pwa-codebuild
Copy link
Collaborator

devops-pwa-codebuild commented May 18, 2020

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
https://pr-2402.pwa-venia.com/venia-tops.html : LH Performance Expected 0.75 Actual 0.35, LH Best Practices Expected 1 Actual 0.92
https://pr-2402.pwa-venia.com/valeria-two-layer-tank.html : LH Performance Expected 0.8 Actual 0.49, LH Accessibility Expected 0.9 Actual 0.89, LH Best Practices Expected 1 Actual 0.92

@supernova-at supernova-at added the version: Minor This changeset includes functionality added in a backwards compatible manner. label May 18, 2020
const bodyContents =
displayState === displayStates.INITIALIZING
? initializingContents
: editingContents;
Copy link
Contributor Author

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.

@m2-community-project m2-community-project bot moved this from Ready for Review to Review in Progress in Pull Request Progress May 18, 2020

// If an authenticated user does not have a preferred shipping method,
// auto-select the least expensive one for them.
useEffect(() => {
Copy link
Contributor Author

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.

@supernova-at
Copy link
Contributor Author

I should note that the Update Modal also has a new behavior:

The call to get all available shipping methods is cache-and-network, and previously this modal was showing a loading spinner until the network responded. Now, the modal shows the data that is in the cache (if present) immediately - it no longer waits for the network to respond. This results in a better, faster experience for the user.

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.

@soumya-ashok
Copy link

@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?

@supernova-at
Copy link
Contributor Author

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 👍

Copy link
Contributor

@tjwiebell tjwiebell left a 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(
Copy link
Contributor

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
Copy link
Contributor

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.

Suggested change
: 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 => {
Copy link
Contributor

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({
Copy link
Contributor

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.

Copy link
Contributor

@sirugh sirugh May 21, 2020

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];
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 unreachable without an address set, but still good to guard against shipping_addresses being null.

Copy link
Contributor

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);
Copy link
Contributor

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([]);
Copy link
Contributor

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:

  1. 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]);
  1. 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.

Copy link
Contributor

Choose a reason for hiding this comment

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

We use the derived pattern often throughout the codebase. Here it is in the shipping info talon and here it is in the breadcrumb talon.

Efficiency aside I think it's more readable which is why I'd prefer it :)

const bodyContents =
displayState === displayStates.INITIALIZING
? initializingContents
: editingContents;
Copy link
Contributor

@tjwiebell tjwiebell May 20, 2020

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) {
Copy link
Contributor

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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Butting whom?

@supernova-at
Copy link
Contributor Author

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 💪

Copy link
Contributor

@tjwiebell tjwiebell left a 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.

Comment on lines +108 to +111
const isBackgroundAutoSelecting =
isSignedIn &&
!derivedSelectedShippingMethod &&
Boolean(derivedShippingMethods.length);
Copy link
Contributor

Choose a reason for hiding this comment

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

👌

@dpatil-magento
Copy link
Contributor

Could not test edit address flow for Auth users but other than that it look good.

@dpatil-magento dpatil-magento merged commit ec0b9ca into develop May 26, 2020
@m2-community-project m2-community-project bot moved this from Review in Progress to Done in Pull Request Progress May 26, 2020
@dpatil-magento dpatil-magento deleted the supernova/573_ship_shortcut branch May 26, 2020 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg:peregrine pkg:venia-ui version: Minor This changeset includes functionality added in a backwards compatible manner.
Development

Successfully merging this pull request may close these issues.

None yet

9 participants