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

Cart and session restore with payment gateways #2231

Draft
wants to merge 6 commits into
base: canary
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/magento-graphcms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@graphcommerce/magento-customer": "8.0.3-canary.7",
"@graphcommerce/magento-graphql": "8.0.3-canary.7",
"@graphcommerce/magento-newsletter": "8.0.3-canary.7",
"@graphcommerce/magento-payment-adyen": "8.0.3-canary.7",
"@graphcommerce/magento-payment-included": "8.0.3-canary.7",
"@graphcommerce/magento-product": "8.0.3-canary.7",
"@graphcommerce/magento-product-bundle": "8.0.3-canary.7",
Expand Down
7 changes: 4 additions & 3 deletions examples/magento-graphcms/pages/checkout/payment.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComposedForm, WaitForQueries } from '@graphcommerce/ecommerce-ui'
import { ComposedForm } from '@graphcommerce/ecommerce-ui'
import { PageOptions } from '@graphcommerce/framer-next-pages'
import {
ApolloCartErrorFullPage,
Expand All @@ -16,6 +16,7 @@ import {
useCartLock,
PaymentMethodActionCardListForm,
PaymentMethodContextProvider,
WaitForPaymentQueries,
} from '@graphcommerce/magento-cart-payment-method'
import { SubscribeToNewsletter } from '@graphcommerce/magento-newsletter'
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
Expand Down Expand Up @@ -49,7 +50,7 @@ function PaymentPage() {
<ComposedForm>
<PageMeta title={i18n._(/* i18n */ 'Payment')} metaRobots={['noindex']} />

<WaitForQueries
<WaitForPaymentQueries
waitFor={[billingPage]}
fallback={
<FullPageMessage icon={<CircularProgress />} title={<Trans id='Loading' />}>
Expand Down Expand Up @@ -141,7 +142,7 @@ function PaymentPage() {
</Container>
</>
)}
</WaitForQueries>
</WaitForPaymentQueries>
</ComposedForm>
)
}
Expand Down
1 change: 1 addition & 0 deletions packages/magento-cart-payment-method/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './PaymentMethodPlaceOrder/PaymentMethodPlaceOrder'
export * from './PaymentMethodPlaceOrderNoop/PaymentMethodPlaceOrderNoop'
export * from './PaymentMethodToggles/PaymentMethodToggles'
export * from './PaymentMethodActionCardList/PaymentMethodActionCardListForm'
export { WaitForQueries as WaitForPaymentQueries } from '@graphcommerce/ecommerce-ui'
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMutation } from '@graphcommerce/graphql'
import { useAssignCurrentCartId } from '@graphcommerce/magento-cart'
import { useApolloClient, useMutation } from '@graphcommerce/graphql'
import { useAssignCurrentCartId, useCurrentCartId } from '@graphcommerce/magento-cart'
import {
PaymentHandlerProps,
usePaymentMethodContext,
Expand All @@ -9,32 +9,52 @@ import { Trans } from '@lingui/react'
import { useEffect } from 'react'
import { useMSPCartLock } from '../../hooks/useMSPCartLock'
import { MSPPaymentHandlerDocument } from './MSPPaymentHandler.gql'
import { CustomerTokenDocument } from '@graphcommerce/magento-customer'

export const MSPPaymentHandler = (props: PaymentHandlerProps) => {
const { code } = props
const [lockStatus, , unlock] = useMSPCartLock()
const assignCurrentCartId = useAssignCurrentCartId()
const { onSuccess } = usePaymentMethodContext()

const { cache } = useApolloClient()
const [restore, { error }] = useMutation(MSPPaymentHandlerDocument)

const { justLocked, success, cart_id: cartId, locked, method, order_number } = lockStatus
const {
justLocked,
success,
cart_id: cartId,
locked,
method,
order_number,
customer_token,
} = lockStatus

const canProceed = !(justLocked || !locked || !cartId || method !== code)

// When the payment has failed we restore the current cart
const shouldRestore = canProceed && success !== '1'
useEffect(() => {
if (!shouldRestore) return

if (customer_token)
cache.writeQuery({
query: CustomerTokenDocument,
data: {
customerToken: {
token: customer_token,
valid: true,
createdAt: new Date().toUTCString(),
__typename: 'CustomerToken',
},
},
broadcast: true,
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
restore({ variables: { cartId } }).then(({ data }) => {
if (!data?.getPaymentMeta) return undefined

assignCurrentCartId(data.getPaymentMeta)
return unlock({ success: null })
return unlock({ success: null, order_number: null, customer_token: null })
})
}, [assignCurrentCartId, cartId, restore, shouldRestore, unlock])
}, [assignCurrentCartId, cache, cartId, customer_token, restore, shouldRestore, unlock])

// If successfull we clear it's cart and redirect to the success page.
const shouldRedirect = canProceed && success === '1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useRouter } from 'next/router'
import { useMSPCartLock } from '../../hooks/useMSPCartLock'
import { MSPPaymentHandlerDocument } from '../MSPPaymentHandler/MSPPaymentHandler.gql'
import { MSPPaymentPlaceOrderDocument } from './MSPPaymentPlaceOrder.gql'
import { useCustomerSession } from '@graphcommerce/magento-customer'

export function MSPPaymentPlaceOrder(props: PaymentPlaceOrderProps) {
const { code, step } = props
Expand All @@ -21,6 +22,7 @@ export function MSPPaymentPlaceOrder(props: PaymentPlaceOrderProps) {

const [restoreCart, restoreResult] = useMutation(MSPPaymentHandlerDocument)
const billingPage = useCartQuery(BillingPageDocument)
const { token } = useCustomerSession()

/**
* In the this folder you'll also find a PaymentMethodOptionsNoop.graphql document that is
Expand Down Expand Up @@ -51,6 +53,7 @@ export function MSPPaymentPlaceOrder(props: PaymentPlaceOrderProps) {
await lock({
method: selectedMethod.code,
order_number: result.data?.placeOrder?.order.order_number,
customer_token: token,
})

await new Promise((resolve) => setTimeout(resolve, 1000))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CartLockState, useCartLock } from '@graphcommerce/magento-cart-payment-
type MSPLockState = CartLockState & {
success?: string | null
order_number?: string | null
customer_token?: string | null
}

/**
Expand Down
65 changes: 65 additions & 0 deletions packages/magento-payment-multisafepay/hooks/useMSPCartRevive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useApolloClient, useMutation } from '@graphcommerce/graphql'
import { useAssignCurrentCartId, useCartQuery, useCurrentCartId } from '@graphcommerce/magento-cart'
import { BillingPageDocument } from '@graphcommerce/magento-cart-checkout'
import { CustomerTokenDocument } from '@graphcommerce/magento-customer'
import { useEffect } from 'react'
import { MSPPaymentHandlerDocument } from '../components/MSPPaymentHandler/MSPPaymentHandler.gql'
import { useMSPCartLock } from './useMSPCartLock'

export function useMSPReviveCart() {
const [{ cart_id, method, customer_token }, , unlock] = useMSPCartLock()
const billingPage = useCartQuery(BillingPageDocument, { fetchPolicy: 'cache-only' })
const client = useApolloClient()
const { currentCartId } = useCurrentCartId()
const assignCurrentCartId = useAssignCurrentCartId()
const [restore, { error, loading }] = useMutation(MSPPaymentHandlerDocument)

const reviveNow =
cart_id &&
!billingPage.data?.cart?.id &&
method?.startsWith('multisafepay_') &&
currentCartId !== cart_id

useEffect(() => {
if (!reviveNow) return // eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
// In GraphCommerce 8 the user would automatically be presented with a restore session dialog
if (customer_token) {
client.cache.writeQuery({
query: CustomerTokenDocument,
broadcast: true,
data: {
customerToken: {
__typename: 'CustomerToken',
token: customer_token,
createdAt: new Date().toUTCString(),
valid: true,
},
},
})
} else {
client.cache.evict({ fieldName: 'customerToken' })
}

const restoredId = (await restore({ variables: { cartId: cart_id } })).data?.getPaymentMeta

if (restoredId) assignCurrentCartId(restoredId)

await unlock({ customer_token: null, order_number: null, success: null })
})()
Comment on lines +23 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

Need this for daxtrio

}, [
assignCurrentCartId,
billingPage,
cart_id,
client.cache,
currentCartId,
customer_token,
error,
restore,
reviveNow,
unlock,
])

if (billingPage.data) return !billingPage.loading
return !loading
}
1 change: 1 addition & 0 deletions packages/magento-payment-multisafepay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@graphcommerce/magento-cart-checkout": "^8.0.3-canary.7",
"@graphcommerce/magento-cart-payment-method": "^8.0.3-canary.7",
"@graphcommerce/magento-cart-shipping-address": "^8.0.3-canary.7",
"@graphcommerce/magento-customer": "^8.0.3-canary.7",
"@graphcommerce/magento-product": "^8.0.3-canary.7",
"@graphcommerce/magento-product-configurable": "^8.0.3-canary.7",
"@graphcommerce/magento-store": "^8.0.3-canary.7",
Expand Down
20 changes: 20 additions & 0 deletions packages/magento-payment-multisafepay/plugins/UseMSPReviveCart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { WaitForPaymentQueries } from '@graphcommerce/magento-cart-payment-method'
import type { PluginProps } from '@graphcommerce/next-config'
import { ComponentProps } from 'react'
import { useMSPReviveCart } from '../hooks/useMSPCartRevive'

export const component = 'WaitForPaymentQueries'
export const exported = '@graphcommerce/magento-cart-payment-method'

function UseMSPReviveCart(props: PluginProps<ComponentProps<typeof WaitForPaymentQueries>>) {
const { Prev, waitFor: w, ...rest } = props
const reviving = useMSPReviveCart()

let waitFor: typeof w

if (w) waitFor = [...(Array.isArray(w) ? w : [w]), reviving]
else waitFor = reviving

return <Prev {...rest} waitFor={waitFor} />
}
export const Plugin = UseMSPReviveCart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { PaymentPlaceOrderProps } from '@graphcommerce/magento-cart-payment-meth
import { useRouter } from 'next/router'
import { usePayPalCartLock } from '../../hooks/usePayPalCartLock'
import { PayPalPaymentPlaceOrderDocument } from './PayPalPaymentPlaceOrder.gql'
import { useCustomerSession } from '@graphcommerce/magento-customer'

export function PayPalPaymentPlaceOrder(props: PaymentPlaceOrderProps) {
const { code, step } = props
const [, lock] = usePayPalCartLock()
const { push } = useRouter()
const { token: sessionToken } = useCustomerSession()

const form = useFormGqlMutationCart(PayPalPaymentPlaceOrderDocument, {
onBeforeSubmit: (variables) => ({
Expand All @@ -27,7 +29,7 @@ export function PayPalPaymentPlaceOrder(props: PaymentPlaceOrderProps) {
'Error while starting the PayPal payment, please try again with a different payment method',
)

await lock({ token, method: code, PayerID: null })
await lock({ token, method: code, PayerID: null, customerToken: sessionToken })
// 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
mutation UsePayPalCartRevive($cartId: String!, $paymentMethod: PaymentMethodInput!) {
setPaymentMethodOnCart(input: { cart_id: $cartId, payment_method: $paymentMethod }) {
cart {
__typename
id
email
...BillingAddress
...ShippingAddress
...Coupon
...PaymentMethodContext
...OrderSuccesPage
...PaymentMethodUpdated
}
}
}
1 change: 1 addition & 0 deletions packages/magento-payment-paypal/hooks/usePayPalCartLock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CartLockState, useCartLock } from '@graphcommerce/magento-cart-payment-
type PayPalLockState = CartLockState & {
token?: string | null
PayerID: string | null
customerToken?: string | null
}

/**
Expand Down
77 changes: 77 additions & 0 deletions packages/magento-payment-paypal/hooks/usePayPalCartRevive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useApolloClient, useMutation } from '@graphcommerce/graphql'
import { useCartQuery } from '@graphcommerce/magento-cart'
import { BillingPageDocument } from '@graphcommerce/magento-cart-checkout'
import { CustomerTokenDocument } from '@graphcommerce/magento-customer'
import { useEffect } from 'react'
import { PayPalPaymentHandlerDocument } from '../components/PayPalPaymentHandler/PayPalPaymentHandler.gql'
import { usePayPalCartLock } from './usePayPalCartLock'
import { UsePayPalCartReviveDocument } from '../graphql/UsePayPalCartRevive.gql'

type UsePayPalCartReviveProps = {
code: string | null
}

export function usePayPalCartRevive(props: UsePayPalCartReviveProps) {
const { code } = props
const [lockStatus] = usePayPalCartLock()

const { token, PayerID, customerToken, method, cart_id: cartId } = lockStatus
const [placeOrder, result] = useMutation(UsePayPalCartReviveDocument)

const billingPage = useCartQuery(BillingPageDocument, { fetchPolicy: 'cache-only' })
const client = useApolloClient()

const reviveNow =
cartId &&
!billingPage.data?.cart?.id &&
method?.startsWith('paypal_') &&
PayerID &&
token &&
code

useEffect(() => {
if (!reviveNow) return // eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
// In GraphCommerce 8 the user would automatically be presented with a restore session dialog
if (customerToken) {
client.cache.writeQuery({
query: CustomerTokenDocument,
broadcast: true,
data: {
customerToken: {
__typename: 'CustomerToken',
token: customerToken,
createdAt: new Date().toUTCString(),
valid: true,
},
},
})
} else {
client.cache.evict({ fieldName: 'customerToken' })
}

const cart = (
await placeOrder({
variables: {
cartId,
paymentMethod: { code, paypal_express: { token, payer_id: PayerID } },
},
})
).data?.setPaymentMethodOnCart?.cart

if (!cart) {
console.log('Could not revive cart')
return
}
client.cache.writeQuery({
query: BillingPageDocument,
variables: { cartId },
data: { cart },
broadcast: true,
})
})()
}, [PayerID, cartId, client.cache, code, customerToken, placeOrder, reviveNow, token])

if (billingPage.data) return !billingPage.loading
return !result.loading
}
2 changes: 2 additions & 0 deletions packages/magento-payment-paypal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"@graphcommerce/graphql": "^8.0.3-canary.7",
"@graphcommerce/image": "^8.0.3-canary.7",
"@graphcommerce/magento-cart": "^8.0.3-canary.7",
"@graphcommerce/magento-cart-checkout": "^8.0.3-canary.7",
"@graphcommerce/magento-cart-payment-method": "^8.0.3-canary.7",
"@graphcommerce/magento-customer": "^8.0.3-canary.7",
"@graphcommerce/magento-store": "^8.0.3-canary.7",
"@graphcommerce/next-ui": "^8.0.3-canary.7",
"@graphcommerce/prettier-config-pwa": "^8.0.3-canary.7",
Expand Down
21 changes: 21 additions & 0 deletions packages/magento-payment-paypal/plugins/UsePayPalReviveCart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { WaitForPaymentQueries } from '@graphcommerce/magento-cart-payment-method'
import type { PluginProps } from '@graphcommerce/next-config'
import { ComponentProps } from 'react'
import { usePayPalCartRevive } from '../hooks/usePayPalCartRevive'

export const component = 'WaitForPaymentQueries'
export const exported = '@graphcommerce/magento-cart-payment-method'

function UsePayPalReviveCart(props: PluginProps<ComponentProps<typeof WaitForPaymentQueries>>) {
const { Prev, waitFor: w, ...rest } = props

const reviving = usePayPalCartRevive({ code: 'paypal_express' })

let waitFor: typeof w

if (w) waitFor = [...(Array.isArray(w) ? w : [w]), reviving]
else waitFor = reviving

return <Prev {...rest} waitFor={waitFor} />
}
export const Plugin = UsePayPalReviveCart
Loading