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

Feat/klarna afterpay #2413

Open
wants to merge 14 commits into
base: canary
Choose a base branch
from
16 changes: 16 additions & 0 deletions packages/magento-payment-afterpay/README.md
Original file line number Diff line number Diff line change
@@ -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/[email protected]` (replace 1.2.3 with
the version of the step above)

This package uses GraphCommerce plugin systems, so there is no code modification
required.
Original file line number Diff line number Diff line change
@@ -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 (
<ActionCard
{...props}
image={
<Image
layout='fixed'
sx={{ width: iconSize, height: iconSize, objectFit: 'contain' }}
unoptimized
src={afterpayMark}
/>
}
/>
)
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <ApolloErrorSnackbar error={error} />
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation AfterpayPaymentPlaceOrder($cartId: String!, $redirect_path: AfterpayRedirectPathInput!) {
createAfterpayCheckout(input: { cart_id: $cartId, redirect_path: $redirect_path }) {
afterpay_redirectCheckoutUrl
afterpay_token
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<form onSubmit={submit} noValidate>
<input type='hidden' value={code} />
</form>
)
}
15 changes: 15 additions & 0 deletions packages/magento-payment-afterpay/hooks/useAfterpayCartLock.ts
Original file line number Diff line number Diff line change
@@ -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<AfterpayLockState>()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/magento-payment-afterpay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
4 changes: 4 additions & 0 deletions packages/magento-payment-afterpay/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
/// <reference types="@graphcommerce/next-ui/types" />
33 changes: 33 additions & 0 deletions packages/magento-payment-afterpay/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
25 changes: 25 additions & 0 deletions packages/magento-payment-afterpay/plugins/AddAfterpayMethods.tsx
Original file line number Diff line number Diff line change
@@ -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<PaymentMethodContextProviderProps>) {
const { modules, Prev, ...rest } = props
return <Prev {...rest} modules={{ ...modules, afterpay }} />
}

export const Plugin = AddAfterpayMethods
5 changes: 5 additions & 0 deletions packages/magento-payment-afterpay/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"exclude": ["node_modules"],
"include": ["**/*.ts", "**/*.tsx"],
"extends": "@graphcommerce/typescript-config-pwa/nextjs.json"
}
12 changes: 0 additions & 12 deletions packages/magento-payment-klarna/KlarnaPaymentMethod.graphql

This file was deleted.

2 changes: 0 additions & 2 deletions packages/magento-payment-klarna/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Script src='https://x.klarnacdn.net/kp/lib/v1/api.js' strategy='lazyOnload' />
<ActionCard
{...props}
image={
<Image
layout='fixed'
sx={{ width: iconSize, height: iconSize, objectFit: 'contain' }}
unoptimized
src={klarnaMark}
/>
}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mutation KlarnaPaymentSession($cartId: String!) {
mutation KlarnaPaymentOptions($cartId: String!) {
createKlarnaPaymentsSession(input: { cart_id: $cartId }) {
client_token
payment_method_categories {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useFormCompose, useFormAutoSubmit } from '@graphcommerce/ecommerce-ui'
import { useFormGqlMutationCart } from '@graphcommerce/magento-cart'
import { PaymentOptionsProps } from '@graphcommerce/magento-cart-payment-method'
import { KlarnaPaymentOptionsDocument } from './KlarnaPaymentOptions.gql'

declare var Klarna: any

export function KlarnaPaymentOptions(props: PaymentOptionsProps) {
const { code, step } = props

const form = useFormGqlMutationCart(KlarnaPaymentOptionsDocument, {
onBeforeSubmit: (variables) => ({
...variables,
}),
onComplete: async (result) => {
if (result.errors) return

const clientToken = result.data?.createKlarnaPaymentsSession?.client_token

try {
Klarna.Payments.init({ client_token: clientToken })
Klarna.Payments.load(
{
container: '#klarna-payments-container',
},
{},
function (res) {
console.debug(res)
},
)
} catch (e) {
console.error(e)
}
},
mode: 'onChange',
})

const submit = form.handleSubmit(() => {})
useFormAutoSubmit({ form, submit, forceInitialSubmit: true })

const key = `PaymentMethodOptions_${code}`
useFormCompose({ form, step, submit, key })

return (
<form onSubmit={submit} noValidate>
<input type='hidden' value={code} />
<div id='klarna-payments-container' />
</form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mutation KlarnaPaymentPlaceOrder($cartId: String!, $paymentMethod: PaymentMethodInput!) {
setPaymentMethodOnCart(input: { cart_id: $cartId, payment_method: $paymentMethod }) {
cart {
...PaymentMethodUpdated
}
}
placeOrder(input: { cart_id: $cartId }) {
order {
order_number
}
}
}
Loading
Loading