magento / pwa-studio

šŸ› Development tools to build, optimize and deploy Progressive Web Applications for Magento 2.
https://developer.adobe.com/commerce/pwa-studio/
Open Software License 3.0
1.06k stars 682 forks source link

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

Closed MichaelHeinzman closed 4 weeks ago

MichaelHeinzman commented 6 months ago

Discussed in https://github.com/magento/pwa-studio/discussions/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 }; }; ```
m2-assistant[bot] commented 6 months ago

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 commented 4 months ago

@adobe export issue to Jira project PWA

github-jira-sync-bot commented 4 months ago

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