diff --git a/packages/magento-payment-afterpay/README.md b/packages/magento-payment-afterpay/README.md new file mode 100644 index 0000000000..65b9796489 --- /dev/null +++ b/packages/magento-payment-afterpay/README.md @@ -0,0 +1,16 @@ +# Magento Payment Afterpay + +Integrates GraphCommerce with the Magento Afterpay module. + +It requires [Afterpay](https://github.com/afterpay/afterpay-magento-2) module to +be installed before using this package. + +## Installation + +1. Find current version of your `@graphcommerce/magento-cart-payment-method` in + your package.json. +2. `yarn add @graphcommerce/magento-payment-afterpay@1.2.3` (replace 1.2.3 with + the version of the step above) + +This package uses GraphCommerce plugin systems, so there is no code modification +required. diff --git a/packages/magento-payment-afterpay/components/AfterpayPaymentActionCard/AfterpayPaymentActionCard.tsx b/packages/magento-payment-afterpay/components/AfterpayPaymentActionCard/AfterpayPaymentActionCard.tsx new file mode 100644 index 0000000000..6e2a72c23f --- /dev/null +++ b/packages/magento-payment-afterpay/components/AfterpayPaymentActionCard/AfterpayPaymentActionCard.tsx @@ -0,0 +1,22 @@ +import { Image } from '@graphcommerce/image' +import { PaymentMethodActionCardProps } from '@graphcommerce/magento-cart-payment-method' +import { ActionCard, useIconSvgSize } from '@graphcommerce/next-ui' +import afterpayMark from '../../icons/afterpay.png' + +export function AfterpayPaymentActionCard(props: PaymentMethodActionCardProps) { + const iconSize = useIconSvgSize('large') + + return ( + + } + /> + ) +} diff --git a/packages/magento-payment-afterpay/components/AfterpayPaymentHandler/AfterpayPaymentHandler.graphql b/packages/magento-payment-afterpay/components/AfterpayPaymentHandler/AfterpayPaymentHandler.graphql new file mode 100644 index 0000000000..03a95a9dd8 --- /dev/null +++ b/packages/magento-payment-afterpay/components/AfterpayPaymentHandler/AfterpayPaymentHandler.graphql @@ -0,0 +1,12 @@ +mutation AfterpayPaymentHandler($cartId: String!, $paymentMethod: PaymentMethodInput!) { + setPaymentMethodOnCart(input: { cart_id: $cartId, payment_method: $paymentMethod }) { + cart { + ...PaymentMethodUpdated + } + } + placeOrder(input: { cart_id: $cartId }) { + order { + order_number + } + } +} diff --git a/packages/magento-payment-afterpay/components/AfterpayPaymentHandler/AfterpayPaymentHandler.tsx b/packages/magento-payment-afterpay/components/AfterpayPaymentHandler/AfterpayPaymentHandler.tsx new file mode 100644 index 0000000000..68c555406a --- /dev/null +++ b/packages/magento-payment-afterpay/components/AfterpayPaymentHandler/AfterpayPaymentHandler.tsx @@ -0,0 +1,62 @@ +import { ApolloErrorSnackbar } from '@graphcommerce/ecommerce-ui' +import { useMutation } from '@graphcommerce/graphql' +import { useCurrentCartId } from '@graphcommerce/magento-cart' +import { + PaymentHandlerProps, + usePaymentMethodContext, +} from '@graphcommerce/magento-cart-payment-method' +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { useAfterpayCartLock } from '../../hooks/useAfterpayCartLock' +import { AfterpayPaymentHandlerDocument } from './AfterpayPaymentHandler.gql' + +export const AfterpayPaymentHandler = (props: PaymentHandlerProps) => { + const { code } = props + const { push } = useRouter() + const [lockStatus, , unlock] = useAfterpayCartLock() + const { onSuccess } = usePaymentMethodContext() + const { currentCartId: cartId } = useCurrentCartId() + + const { orderToken, status, locked, justLocked, method } = lockStatus + const [placeOrder, { error, called }] = useMutation(AfterpayPaymentHandlerDocument, { + variables: { + cartId, + paymentMethod: { + code, + afterpay: { + afterpay_token: orderToken ?? '', + }, + }, + }, + errorPolicy: 'all', + }) + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + if (locked && !justLocked && status === 'CANCELLED') unlock({ orderToken: null, status: null }) + }, [status, code, justLocked, locked, method, unlock]) + + // If successfull we clear it's cart and redirect to the success page. + useEffect(() => { + if (!orderToken || status !== 'SUCCESS' || called) return + if (!cartId) return + + const fetchData = async () => { + const res = await placeOrder() + + if (res.errors || !res.data?.placeOrder?.order) { + await unlock({ orderToken: null, status: null }) + return + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + onSuccess(res.data?.placeOrder?.order.order_number) + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchData() + }, [status, called, cartId, onSuccess, placeOrder, push, orderToken, unlock]) + + if (error) return + return null +} diff --git a/packages/magento-payment-afterpay/components/AfterpayPaymentPlaceOrder/AfterpayPaymentPlaceOrder.graphql b/packages/magento-payment-afterpay/components/AfterpayPaymentPlaceOrder/AfterpayPaymentPlaceOrder.graphql new file mode 100644 index 0000000000..6071685fe8 --- /dev/null +++ b/packages/magento-payment-afterpay/components/AfterpayPaymentPlaceOrder/AfterpayPaymentPlaceOrder.graphql @@ -0,0 +1,6 @@ +mutation AfterpayPaymentPlaceOrder($cartId: String!, $redirect_path: AfterpayRedirectPathInput!) { + createAfterpayCheckout(input: { cart_id: $cartId, redirect_path: $redirect_path }) { + afterpay_redirectCheckoutUrl + afterpay_token + } +} diff --git a/packages/magento-payment-afterpay/components/AfterpayPaymentPlaceOrder/AfterpayPaymentPlaceOrder.tsx b/packages/magento-payment-afterpay/components/AfterpayPaymentPlaceOrder/AfterpayPaymentPlaceOrder.tsx new file mode 100644 index 0000000000..029a654c88 --- /dev/null +++ b/packages/magento-payment-afterpay/components/AfterpayPaymentPlaceOrder/AfterpayPaymentPlaceOrder.tsx @@ -0,0 +1,44 @@ +import { useFormCompose } from '@graphcommerce/ecommerce-ui' +import { useFormGqlMutationCart } from '@graphcommerce/magento-cart' +import { PaymentPlaceOrderProps } from '@graphcommerce/magento-cart-payment-method' +import { useRouter } from 'next/router' +import { useAfterpayCartLock } from '../../hooks/useAfterpayCartLock' +import { AfterpayPaymentPlaceOrderDocument } from './AfterpayPaymentPlaceOrder.gql' + +export function AfterpayPaymentPlaceOrder(props: PaymentPlaceOrderProps) { + const { code, step } = props + const [, lock] = useAfterpayCartLock() + const { push } = useRouter() + + const form = useFormGqlMutationCart(AfterpayPaymentPlaceOrderDocument, { + onBeforeSubmit: (variables) => ({ + ...variables, + redirect_path: { cancel_path: `checkout/payment`, confirm_path: `checkout/payment` }, + }), + onComplete: async (result) => { + if (result.errors) return + + const start = result.data?.createAfterpayCheckout?.afterpay_redirectCheckoutUrl + const orderToken = result.data?.createAfterpayCheckout?.afterpay_token + + if (!start) + throw Error( + 'Error while starting the Afterpay payment, please try again with a different payment method', + ) + + await lock({ orderToken, method: code }) + // We are going to redirect, but we're not waiting, because we need to complete the submission to release the buttons + // eslint-disable-next-line @typescript-eslint/no-floating-promises + push(start) + }, + }) + + const submit = form.handleSubmit(() => {}) + useFormCompose({ form, step, submit, key: `AfterpayPaymentPlaceOrder_${code}` }) + + return ( +
+ +
+ ) +} diff --git a/packages/magento-payment-afterpay/hooks/useAfterpayCartLock.ts b/packages/magento-payment-afterpay/hooks/useAfterpayCartLock.ts new file mode 100644 index 0000000000..adf0ea4c91 --- /dev/null +++ b/packages/magento-payment-afterpay/hooks/useAfterpayCartLock.ts @@ -0,0 +1,15 @@ +import { CartLockState, useCartLock } from '@graphcommerce/magento-cart-payment-method' + +type AfterpayLockState = CartLockState & { + orderToken?: string | null + status?: 'SUCCESS' | 'CANCELLED' | null +} + +/** + * The cart lock situation is a bit odd since are unable to actually influence the return URL we + * can't safely remember the cart ID. + * + * This is a potential bug because when the customer is returning from an icognito session, the cart + * ID is not available. + */ +export const useAfterpayCartLock = () => useCartLock() diff --git a/packages/magento-payment-afterpay/icons/afterpay.png b/packages/magento-payment-afterpay/icons/afterpay.png new file mode 100644 index 0000000000..2bd2eca0b1 Binary files /dev/null and b/packages/magento-payment-afterpay/icons/afterpay.png differ diff --git a/packages/magento-payment-afterpay/index.ts b/packages/magento-payment-afterpay/index.ts new file mode 100644 index 0000000000..336ce12bb9 --- /dev/null +++ b/packages/magento-payment-afterpay/index.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/magento-payment-afterpay/next-env.d.ts b/packages/magento-payment-afterpay/next-env.d.ts new file mode 100644 index 0000000000..799156c1ad --- /dev/null +++ b/packages/magento-payment-afterpay/next-env.d.ts @@ -0,0 +1,4 @@ +/// +/// +/// +/// diff --git a/packages/magento-payment-afterpay/package.json b/packages/magento-payment-afterpay/package.json new file mode 100644 index 0000000000..0fb14dbb04 --- /dev/null +++ b/packages/magento-payment-afterpay/package.json @@ -0,0 +1,33 @@ +{ + "name": "@graphcommerce/magento-payment-afterpay", + "homepage": "https://www.graphcommerce.org/", + "repository": "github:graphcommerce-org/graphcommerce", + "version": "9.0.0-canary.80", + "private": true, + "sideEffects": false, + "prettier": "@graphcommerce/prettier-config-pwa", + "eslintConfig": { + "extends": "@graphcommerce/eslint-config-pwa", + "parserOptions": { + "project": "./tsconfig.json" + } + }, + "peerDependencies": { + "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.80", + "@graphcommerce/graphql": "^9.0.0-canary.80", + "@graphcommerce/image": "^9.0.0-canary.80", + "@graphcommerce/magento-cart": "^9.0.0-canary.80", + "@graphcommerce/magento-store": "^9.0.0-canary.80", + "@graphcommerce/next-ui": "^9.0.0-canary.80", + "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.80", + "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.80", + "@lingui/core": "^4.2.1", + "@lingui/macro": "^4.2.1", + "@lingui/react": "^4.2.1", + "@mui/material": "^5.10.16", + "framer-motion": "^10.0.0", + "next": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/packages/magento-payment-afterpay/plugins/AddAfterpayMethods.tsx b/packages/magento-payment-afterpay/plugins/AddAfterpayMethods.tsx new file mode 100644 index 0000000000..cbf7ef5a87 --- /dev/null +++ b/packages/magento-payment-afterpay/plugins/AddAfterpayMethods.tsx @@ -0,0 +1,25 @@ +import { + PaymentMethodContextProviderProps, + PaymentMethodOptionsNoop, +} from '@graphcommerce/magento-cart-payment-method' +import type { PluginProps } from '@graphcommerce/next-config' +import { AfterpayPaymentActionCard } from '../components/AfterpayPaymentActionCard/AfterpayPaymentActionCard' +import { AfterpayPaymentHandler } from '../components/AfterpayPaymentHandler/AfterpayPaymentHandler' +import { AfterpayPaymentPlaceOrder } from '../components/AfterpayPaymentPlaceOrder/AfterpayPaymentPlaceOrder' + +export const component = 'PaymentMethodContextProvider' +export const exported = '@graphcommerce/magento-cart-payment-method' + +const afterpay = { + PaymentOptions: PaymentMethodOptionsNoop, + PaymentActionCard: AfterpayPaymentActionCard, + PaymentHandler: AfterpayPaymentHandler, + PaymentPlaceOrder: AfterpayPaymentPlaceOrder, +} + +function AddAfterpayMethods(props: PluginProps) { + const { modules, Prev, ...rest } = props + return +} + +export const Plugin = AddAfterpayMethods diff --git a/packages/magento-payment-afterpay/tsconfig.json b/packages/magento-payment-afterpay/tsconfig.json new file mode 100644 index 0000000000..1fc364d21b --- /dev/null +++ b/packages/magento-payment-afterpay/tsconfig.json @@ -0,0 +1,5 @@ +{ + "exclude": ["node_modules"], + "include": ["**/*.ts", "**/*.tsx"], + "extends": "@graphcommerce/typescript-config-pwa/nextjs.json" +} diff --git a/packages/magento-payment-klarna/KlarnaPaymentMethod.graphql b/packages/magento-payment-klarna/KlarnaPaymentMethod.graphql deleted file mode 100644 index 52c24adf6f..0000000000 --- a/packages/magento-payment-klarna/KlarnaPaymentMethod.graphql +++ /dev/null @@ -1,12 +0,0 @@ -mutation KlarnaPaymentMethod($cartId: String!, $authorizationToken: String!) { - setPaymentMethodOnCart( - input: { - cart_id: $cartId - payment_method: { code: "klarna", klarna: { authorization_token: $authorizationToken } } - } - ) { - cart { - ...PaymentMethodUpdated - } - } -} diff --git a/packages/magento-payment-klarna/README.md b/packages/magento-payment-klarna/README.md index 2d8280606f..e04694a295 100644 --- a/packages/magento-payment-klarna/README.md +++ b/packages/magento-payment-klarna/README.md @@ -2,8 +2,6 @@ Integrates GraphCommerce with the Magento Klarna module. -Note: Not yet working - ## Installation 1. Find current version of your `@graphcommerce/magento-cart-payment-method` in diff --git a/packages/magento-payment-klarna/components/KlarnaPaymentActionCard/KlarnaPaymentActionCard.tsx b/packages/magento-payment-klarna/components/KlarnaPaymentActionCard/KlarnaPaymentActionCard.tsx new file mode 100644 index 0000000000..ae55b55e9c --- /dev/null +++ b/packages/magento-payment-klarna/components/KlarnaPaymentActionCard/KlarnaPaymentActionCard.tsx @@ -0,0 +1,26 @@ +import Script from 'next/script' +import { Image } from '@graphcommerce/image' +import { PaymentMethodActionCardProps } from '@graphcommerce/magento-cart-payment-method' +import { ActionCard, useIconSvgSize } from '@graphcommerce/next-ui' +import klarnaMark from '../../icons/klarna.png' + +export function KlarnaPaymentActionCard(props: PaymentMethodActionCardProps) { + const iconSize = useIconSvgSize('large') + + return ( + <> +