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

[feature]: Extending Peregrine Talons for Custom Applications #4239

Open
MichaelHeinzman opened this issue Mar 4, 2024 Discussed in #4234 · 3 comments
Open

[feature]: Extending Peregrine Talons for Custom Applications #4239

MichaelHeinzman opened this issue Mar 4, 2024 Discussed in #4234 · 3 comments

Comments

@MichaelHeinzman
Copy link

MichaelHeinzman commented Mar 4, 2024

Discussed in #4234

Originally posted by MichaelHeinzman February 19, 2024

I am aware there is a current implementation to extend components, I can't utilize that implementation in our application.

Background

I'm currently working on a website that utilizes magento peregrine talons in AEM ui.frontend. We have a mix of custom code where we took the talons outlined in pwa studio and made them our own. However, this got us in trouble when we needed to upgrade peregrine and certain features of our application. We had to go line by line and check if we needed to change the customized components to align more with the updated components. It took a while and we still have scattered customized components across our application.

Going line by line isn't a long-term solution to updating magento peregrine talons. I would rather know exactly what functions and variables we customized, and have it be in one file, not mixed with the magento peregrine code. This was why I thought about wrapping the talon in a hook, changing only the values and functions we needed to change, so that I can see clearly the customized code. However, I can't do this approach since not all of the values in the talon used are returned. I could repeat declarations but that would defeat the point of the wrapper.

What I am wondering

Can an update to the talons be made where they return all of the values in the talon.

For example, right now I'm trying to create hook called useSignIn.js that extends the useSignIn talon from peregrine. Since we use a different handleSubmit I was going to return our custom handleSubmit that we set in the wrapper using the values from the peregrine talon.

But, there are certain mutations and values that I can't access such as the signIn mutation, since it's not returned.

I'm forced to copy all of the code from the useSignIn talon, make a new file and customize that file. This implementation has already failed us when we upgraded @magento/peregrine and different packages, which led to half of our site breaking.

If we could wrap the package instead of creating a new file with a customized copy, then we can pick and choose what we want customized, it would be immutable, and when we upgrade in the future it would limit the damages. We would also be able to see exactly what has changed without having to worry about if we customized too much.

Example Extended / Customized useSignIn.js hook

Here's my implementation:

import { useSignIn as useSignInPeregrine } from '@magento/peregrine/lib/talons/SignIn/useSignIn';
// I ignored imports needed to shorten the example. 

const useSignIn = props => {
   const values = useSignInPeregrine(props);

   // The constants returned. I ignored the already returned values so I can showcase what I mean.
   // These can be used in the custom handleSubmit or other functions in this hook.
   const { signIn, fetchUserDetails, fetchCartDetails, fetchCartId, mergeCarts, isSigningIn, setIsSigningIn, cartContext, userContext, eventingContext, apolloClient, etc... } = values;

   // Custom handleSubmit. 
   const handleSubmit = useCallback(() => {});
   return { ...values, handleSubmit };
};

export default useSignIn;

As of right now, the only returned values from the useSignIn talon are

    return {
        errors,
        handleCreateAccount,
        handleEnterKeyPress,
        signinHandleEnterKeyPress,
        handleForgotPassword,
        forgotPasswordHandleEnterKeyPress,
        handleSubmit,
        isBusy: isGettingDetails || isSigningIn || recaptchaLoading,
        setFormApi,
        recaptchaWidgetProps
    };

Of course I'm not just talking about the useSignIn hook, this would be useful for customization of all of the talons.

Example peregrine useSignIn.js talon update.

Here is an example of changes I propose to make to the useSignIn.js talon in peregrine library, that would allow it to be wrappable.

export const useSignIn = props => {
    const {
        handleTriggerClick,
        getCartDetailsQuery,
        setDefaultUsername,
        showCreateAccount,
        showForgotPassword
    } = props;

    const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
    const {
        createCartMutation,
        getCustomerQuery,
        mergeCartsMutation,
        signInMutation
    } = operations;

    const apolloClient = useApolloClient();
    const [isSigningIn, setIsSigningIn] = useState(false);

    const cartContext  = useCartContext();
    const [
        { cartId },
        { createCart, removeCart, getCartDetails }
    ] = cartContext;

    const userContext = useUserContext();
    const [
        { isGettingDetails, getDetailsError },
        { getUserDetails, setToken }
    ] = userContext;

    const eventingContext = useEventingContext();
    const [, { dispatch }] = eventingContext;

    const signInMutationResult = useMutation(signInMutation, {
        fetchPolicy: 'no-cache'
    });
    const [signIn, { error: signInError }] = signInMutationResult;

    const googleReCaptcha = useGoogleReCaptcha({
        currentForm: 'CUSTOMER_LOGIN',
        formAction: 'signIn'
    });
    const {
        generateReCaptchaData,
        recaptchaLoading,
        recaptchaWidgetProps
    } = googleReCaptcha

    const [fetchCartId] = useMutation(createCartMutation);
    const [mergeCarts] = useMutation(mergeCartsMutation);
    const fetchUserDetails = useAwaitQuery(getCustomerQuery);
    const fetchCartDetails = useAwaitQuery(getCartDetailsQuery);

    const formApiRef = useRef(null);
    const setFormApi = useCallback(api => (formApiRef.current = api), []);

    const handleSubmit = useCallback(
        async ({ email, password }) => {
            setIsSigningIn(true);
            handleTriggerClick();
            try {
                // Get source cart id (guest cart id).
                const sourceCartId = cartId;

                // Get recaptchaV3 data for login
                const recaptchaData = await generateReCaptchaData();

                // Sign in and set the token.
                const signInResponse = await signIn({
                    variables: {
                        email,
                        password
                    },
                    ...recaptchaData
                });

                const token = signInResponse.data.generateCustomerToken.token;
                await setToken(token);

                // Clear all cart/customer data from cache and redux.
                await apolloClient.clearCacheData(apolloClient, 'cart');
                await apolloClient.clearCacheData(apolloClient, 'customer');
                await removeCart();

                // Create and get the customer's cart id.
                await createCart({
                    fetchCartId
                });
                const destinationCartId = await retrieveCartId();

                // Merge the guest cart into the customer cart.
                await mergeCarts({
                    variables: {
                        destinationCartId,
                        sourceCartId
                    }
                });

                // Ensure old stores are updated with any new data.

                await getUserDetails({ fetchUserDetails });

                const { data } = await fetchUserDetails({
                    fetchPolicy: 'cache-only'
                });

                dispatch({
                    type: 'USER_SIGN_IN',
                    payload: {
                        ...data.customer
                    }
                });

                getCartDetails({ fetchCartId, fetchCartDetails });
            } catch (error) {
                if (process.env.NODE_ENV !== 'production') {
                    console.error(error);
                }

                setIsSigningIn(false);
            }
        },
        [
            cartId,
            generateReCaptchaData,
            signIn,
            setToken,
            apolloClient,
            removeCart,
            createCart,
            fetchCartId,
            mergeCarts,
            getUserDetails,
            fetchUserDetails,
            getCartDetails,
            fetchCartDetails,
            dispatch,
            handleTriggerClick
        ]
    );

    const handleForgotPassword = useCallback(() => {
        const { current: formApi } = formApiRef;

        if (formApi) {
            setDefaultUsername(formApi.getValue('email'));
        }

        showForgotPassword();
    }, [setDefaultUsername, showForgotPassword]);

    const forgotPasswordHandleEnterKeyPress = useCallback(() => {
        event => {
            if (event.key === 'Enter') {
                handleForgotPassword();
            }
        };
    }, [handleForgotPassword]);

    const handleCreateAccount = useCallback(() => {
        const { current: formApi } = formApiRef;

        if (formApi) {
            setDefaultUsername(formApi.getValue('email'));
        }

        showCreateAccount();
    }, [setDefaultUsername, showCreateAccount]);

    const handleEnterKeyPress = useCallback(() => {
        event => {
            if (event.key === 'Enter') {
                handleCreateAccount();
            }
        };
    }, [handleCreateAccount]);

    const signinHandleEnterKeyPress = useCallback(() => {
        event => {
            if (event.key === 'Enter') {
                handleSubmit();
            }
        };
    }, [handleSubmit]);

    const errors = useMemo(
        () =>
            new Map([
                ['getUserDetailsQuery', getDetailsError],
                ['signInMutation', signInError]
            ]),
        [getDetailsError, signInError]
    );

    return {
        errors,
        handleCreateAccount,
        handleEnterKeyPress,
        signinHandleEnterKeyPress,
        handleForgotPassword,
        forgotPasswordHandleEnterKeyPress,
        handleSubmit,
        isBusy: isGettingDetails || isSigningIn || recaptchaLoading,
        setFormApi,
        recaptchaWidgetProps, 

        // New values
        operations,
        apolloClient, 
        isSigningIn,
        setIsSigningIn,
        cartContext,
        userContext,
        eventingContext,
        signInMutationResult,
        googleReCaptcha,
        fetchCartId,
        mergeCarts,
        fetchUserDetails,
        fetchCartDetails
    };
};
```</div>
Copy link

m2-assistant bot commented Mar 4, 2024

Hi @MichaelHeinzman. Thank you for your report.
To speed up processing of this issue, make sure that you provided sufficient information.
Add a comment to assign the issue: @magento I am working on this


Join Magento Community Engineering Slack and ask your questions in #github channel.

@glo82145
Copy link
Collaborator

glo82145 commented May 9, 2024

@adobe export issue to Jira project PWA

@github-jira-sync-bot
Copy link

✅ Jira issue https://jira.corp.adobe.com/browse/PWA-3276 is successfully created for this GitHub issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants