diff --git a/examples/nextjs/app/checkout.tsx b/examples/nextjs/app/checkout.tsx index 9a381042..3478e88a 100644 --- a/examples/nextjs/app/checkout.tsx +++ b/examples/nextjs/app/checkout.tsx @@ -60,6 +60,14 @@ export function CheckoutPage({ session }: { session: CheckoutSession }) { } : undefined } + ccavenueConfig={ + process.env.NEXT_PUBLIC_CCAVENUE_ACCESS_CODE_ID + ? { + accessCodeId: + process.env.NEXT_PUBLIC_CCAVENUE_ACCESS_CODE_ID, + } + : undefined + } /> ); } diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index 7593cdbf..28e0c01c 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -62,6 +62,10 @@ export default async function Home() { processor: 'mercadopago', checkoutTypes: ['standard'], }, + ccavenue: { + processor: 'ccavenue', + checkoutTypes: ['standard'], + }, paypal: { processor: 'paypal', checkoutTypes: ['express', 'standard'], diff --git a/examples/nextjs/app/store/actions.ts b/examples/nextjs/app/store/actions.ts index 1cc06f39..6dbe2207 100644 --- a/examples/nextjs/app/store/actions.ts +++ b/examples/nextjs/app/store/actions.ts @@ -51,6 +51,10 @@ export async function checkoutWithOrder(orderId: string) { processor: 'godaddy', checkoutTypes: ['standard'], }, + ccavenue: { + processor: 'ccavenue', + checkoutTypes: ['standard'], + }, express: { processor: 'godaddy', checkoutTypes: ['express'], diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts index a963947a..11d375a8 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -107,6 +107,7 @@ export const deDe = { paze: 'Paze', offline: 'Offline-Zahlungen', mercadopago: 'Mercado Pago', + ccavenue: 'Mit CCAvenue bezahlen', }, descriptions: { creditCard: '', @@ -117,6 +118,7 @@ export const deDe = { offline: '', mercadopago: 'Verwende das MercadoPago-Formular unten, um deinen Kauf sicher abzuschließen.', + ccavenue: '', }, noMethodsAvailable: 'Keine Zahlungsmethoden verfügbar', cardNumber: 'Kartennummer', diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index 14be3473..450a28d1 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -107,6 +107,7 @@ export const enIe = { paze: 'Paze', offline: 'Offline payments', mercadopago: 'Mercado Pago', + ccavenue: 'Pay with CCAvenue', }, descriptions: { creditCard: '', @@ -117,6 +118,7 @@ export const enIe = { offline: '', mercadopago: 'Use the MercadoPago form below to complete your purchase securely.', + ccavenue: '', }, noMethodsAvailable: 'No payment methods available', cardNumber: 'Card number', diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts index 023741f1..db18689c 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -107,6 +107,7 @@ export const enUs = { paze: 'Paze', offline: 'Offline payments', mercadopago: 'Mercado Pago', + ccavenue: 'Pay with CCAvenue', }, descriptions: { creditCard: '', @@ -117,6 +118,7 @@ export const enUs = { offline: '', mercadopago: 'Use the MercadoPago form below to complete your purchase securely.', + ccavenue: '', }, noMethodsAvailable: 'No payment methods available', cardNumber: 'Card number', diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts index 4d4d4ab7..ae62fa8e 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -108,6 +108,7 @@ export const esAr = { paze: 'Paze', offline: 'Pagos en efectivo', mercadopago: 'Mercado Pago', + ccavenue: 'الدفع عبر CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const esAr = { offline: '', mercadopago: 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', + ccavenue: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts index 03add0ab..6dd7c8fa 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -108,6 +108,7 @@ export const esCl = { paze: 'Paze', offline: 'Pagos offline', mercadopago: 'Mercado Pago', + ccavenue: 'Pagar con CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const esCl = { offline: '', mercadopago: 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', + ccavenue: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts index 7dfb84ce..6fc6b6d8 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -108,6 +108,7 @@ export const esCo = { paze: 'Paze', offline: 'Pagos sin conexión', mercadopago: 'Mercado Pago', + ccavenue: 'Pagar con CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const esCo = { offline: '', mercadopago: 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', + ccavenue: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts index 06af42fa..ff845f89 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -108,6 +108,7 @@ export const esEs = { paze: 'Paze', offline: 'Pagos sin conexión', mercadopago: 'Mercado Pago', + ccavenue: 'Pagar con CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const esEs = { offline: '', mercadopago: 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', + ccavenue: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts index 569aea7d..06f4698d 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -108,6 +108,7 @@ export const esMx = { paze: 'Paze', offline: 'Pagos fuera de línea', mercadopago: 'Mercado Pago', + ccavenue: 'Pagar con CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const esMx = { offline: '', mercadopago: 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', + ccavenue: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts index ba39de69..b8c82659 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -108,6 +108,7 @@ export const esPe = { paze: 'Paze', offline: 'Pagos en efectivo', mercadopago: 'Mercado Pago', + ccavenue: 'Pagar con CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const esPe = { offline: '', mercadopago: 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', + ccavenue: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts index f6aa38b5..e4ab2386 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -108,6 +108,7 @@ export const esUs = { paze: 'Paze', offline: 'Pagos offline', mercadopago: 'Mercado Pago', + ccavenue: 'Pagar con CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const esUs = { offline: '', mercadopago: 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', + ccavenue: '', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts index d4d36f82..d139fb4f 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -108,6 +108,7 @@ export const frCa = { paze: 'Paze', offline: 'Paiements hors ligne', mercadopago: 'Mercado Pago', + ccavenue: 'Payer avec CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const frCa = { offline: '', mercadopago: 'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.', + ccavenue: '', }, noMethodsAvailable: 'Aucune méthode de paiement disponible', cardNumber: 'Numéro de carte', diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts index 8525e8aa..27702e17 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -108,6 +108,7 @@ export const frFr = { paze: 'Paze', offline: 'Paiements hors ligne', mercadopago: 'Mercado Pago', + ccavenue: 'Payer avec CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const frFr = { offline: '', mercadopago: 'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.', + ccavenue: '', }, noMethodsAvailable: 'Aucune méthode de paiement disponible', cardNumber: 'Numéro de carte', diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts index 673beba8..ebbe647d 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -107,6 +107,7 @@ export const idId = { paze: 'Paze', offline: 'Pembayaran offline', mercadopago: 'Mercado Pago', + ccavenue: 'Bayar dengan CCAvenue', }, descriptions: { creditCard: '', @@ -117,6 +118,7 @@ export const idId = { offline: '', mercadopago: 'Gunakan formulir MercadoPago di bawah untuk menyelesaikan pembelian Anda dengan aman.', + ccavenue: '', }, noMethodsAvailable: 'Tidak ada metode pembayaran tersedia', cardNumber: 'Nomor kartu', diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts index eab9b539..fd98b6d3 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -108,6 +108,7 @@ export const itIt = { paze: 'Paze', offline: 'Pagamenti offline', mercadopago: 'Mercado Pago', + ccavenue: 'Paga con CCAvenue', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const itIt = { offline: '', mercadopago: 'Usa il modulo MercadoPago qui sotto per completare l’acquisto in modo sicuro.', + ccavenue: '', }, noMethodsAvailable: 'Nessun metodo di pagamento disponibile', cardNumber: 'Numero della carta', diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts index 0e083448..f4cb7c27 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -107,6 +107,7 @@ export const ptBr = { paze: 'Paze', offline: 'Pagamentos offline', mercadopago: 'Mercado Pago', + ccavenue: 'Pagar com CCAvenue', }, descriptions: { creditCard: '', @@ -117,6 +118,7 @@ export const ptBr = { offline: '', mercadopago: 'Use o formulário do MercadoPago abaixo para concluir sua compra com segurança.', + ccavenue: '', }, noMethodsAvailable: 'Nenhum método de pagamento disponível', cardNumber: 'Número do cartão', diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts index a8db015e..91ad583f 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -108,6 +108,7 @@ export const qaPs = { paze: '[Þâžë Þâÿmëñţ Šërvîçë]', offline: '[Öfflîñë þâÿmëñţ mëţhödš]', mercadopago: 'Mercado Pago', + ccavenue: '[Þâÿ ïñ ÇÇÂvëñûë]', }, descriptions: { creditCard: '', @@ -118,6 +119,7 @@ export const qaPs = { offline: '', mercadopago: '[Üšë ţhë MërçâðöÞâgö förm këlöw ţö çömþlëţë ÿöür þürçhâšë šëçürëlÿ.]', + ccavenue: '', }, noMethodsAvailable: '[Ñö þâÿmëñţ mëţhödš âvâîlâblë âţ ţhîš ţîmë]', cardNumber: '[Çârd ñümkër îñþüţ fîëld]', diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts index 48de023b..339867c4 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -107,6 +107,7 @@ export const trTr = { paze: 'Paze', offline: 'Çevrimdışı ödemeler', mercadopago: 'Mercado Pago', + ccavenue: 'CCAvenue ile öde', }, descriptions: { creditCard: '', @@ -117,6 +118,7 @@ export const trTr = { offline: '', mercadopago: 'Satın alımınızı güvenle tamamlamak için aşağıdaki MercadoPago formunu kullanın.', + ccavenue: '', }, noMethodsAvailable: 'Kullanılabilir ödeme yöntemi yok', cardNumber: 'Kart numarası', diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts index 72fdc757..a496cdc0 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -107,6 +107,7 @@ export const viVn = { paze: 'Paze', offline: 'Thanh toán ngoại tuyến', mercadopago: 'Mercado Pago', + ccavenue: 'Thanh toán bằng CCAvenue', }, descriptions: { creditCard: '', @@ -117,6 +118,7 @@ export const viVn = { offline: '', mercadopago: 'Hãy sử dụng biểu mẫu MercadoPago bên dưới để hoàn tất mua hàng một cách an toàn.', + ccavenue: '', }, noMethodsAvailable: 'Không có phương thức thanh toán nào', cardNumber: 'Số thẻ', diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts index b653202a..d5c0e2a2 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -103,6 +103,7 @@ export const zhCn = { paze: 'Paze', offline: '线下付款', mercadopago: 'Mercado Pago', + ccavenue: '使用 CCAvenue 支付', }, descriptions: { creditCard: '', @@ -112,6 +113,7 @@ export const zhCn = { paze: '', offline: '', mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。', + ccavenue: '', }, noMethodsAvailable: '暂无可用的付款方式', cardNumber: '卡号', diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts index aa8e2a81..de14b637 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -103,6 +103,7 @@ export const zhSg = { paze: 'Paze', offline: '线下付款', mercadopago: 'Mercado Pago', + ccavenue: '使用 CCAvenue 支付', }, descriptions: { creditCard: '', @@ -112,6 +113,7 @@ export const zhSg = { paze: '', offline: '', mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。', + ccavenue: '', }, noMethodsAvailable: '无可用付款方式', cardNumber: '卡号', diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index 8ae642b4..5012b3ed 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -85,6 +85,10 @@ export type MercadoPagoConfig = { country: 'AR' | 'BR' | 'CO' | 'CL' | 'PE' | 'MX'; }; +export type CCAvenueConfig = { + accessCodeId: string; +}; + interface CheckoutContextValue { elements?: CheckoutElements; targets?: Partial< @@ -98,6 +102,7 @@ interface CheckoutContextValue { squareConfig?: SquareConfig; paypalConfig?: PayPalConfig; mercadoPagoConfig?: MercadoPagoConfig; + ccavenueConfig?: CCAvenueConfig; isConfirmingCheckout: boolean; setIsConfirmingCheckout: (isConfirming: boolean) => void; checkoutErrors?: string[] | undefined; @@ -209,6 +214,7 @@ export interface CheckoutProps { squareConfig?: SquareConfig; paypalConfig?: PayPalConfig; mercadoPagoConfig?: MercadoPagoConfig; + ccavenueConfig?: CCAvenueConfig; layout?: LayoutSection[]; direction?: 'ltr' | 'rtl'; showStoreHours?: boolean; @@ -229,10 +235,14 @@ export function Checkout(props: CheckoutProps) { squareConfig, paypalConfig, mercadoPagoConfig, + ccavenueConfig, isCheckoutDisabled, } = props; - const [isConfirmingCheckout, setIsConfirmingCheckout] = React.useState(false); + const [isConfirmingCheckout, setIsConfirmingCheckout] = React.useState(() => { + if (typeof window === 'undefined') return false; + return new URLSearchParams(window.location.search).has('encResp'); + }); const [checkoutErrors, setCheckoutErrors] = React.useState< string[] | undefined >(undefined); @@ -395,6 +405,7 @@ export function Checkout(props: CheckoutProps) { squareConfig, mercadoPagoConfig, paypalConfig, + ccavenueConfig, requiredFields, isConfirmingCheckout, setIsConfirmingCheckout, diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index d9825c11..1655cf4b 100644 --- a/packages/react/src/components/checkout/form/checkout-form.tsx +++ b/packages/react/src/components/checkout/form/checkout-form.tsx @@ -73,13 +73,15 @@ export function CheckoutForm({ }: CheckoutFormProps) { const formatCurrency = useFormatCurrency(); const { t } = useGoDaddyContext(); - const { session, isCheckoutDisabled } = useCheckoutContext(); + const { session, isCheckoutDisabled, isConfirmingCheckout } = + useCheckoutContext(); const form = useForm({ resolver: zodResolver(schema), defaultValues: defaultValues ?? {}, reValidateMode: 'onBlur', mode: 'onBlur', + disabled: isConfirmingCheckout, }); const deliveryMethod = form.watch('deliveryMethod'); diff --git a/packages/react/src/components/checkout/order/use-draft-order-sync.ts b/packages/react/src/components/checkout/order/use-draft-order-sync.ts index d99dd6e9..e06caba7 100644 --- a/packages/react/src/components/checkout/order/use-draft-order-sync.ts +++ b/packages/react/src/components/checkout/order/use-draft-order-sync.ts @@ -240,7 +240,7 @@ export function useDraftOrderFieldSync({ }) { const lastSubmittedRef = React.useRef>({}); const updateDraftOrder = useUpdateOrder(); - const { session } = useCheckoutContext(); + const { session, isConfirmingCheckout } = useCheckoutContext(); const { data: draftOrderData } = useDraftOrder(); const form = useFormContext(); const pendingResetRef = React.useRef([]); @@ -272,7 +272,7 @@ export function useDraftOrderFieldSync({ ); React.useEffect(() => { - if (!enabled) return; + if (!enabled || isConfirmingCheckout) return; const memoKey = key ?? 'default'; const currentSerialized = JSON.stringify(data); @@ -353,6 +353,7 @@ export function useDraftOrderFieldSync({ } ); }, [ + isConfirmingCheckout, enabled, data, mapToInput, diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/ccavenue/ccavenue.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/ccavenue/ccavenue.tsx new file mode 100644 index 00000000..6f1488f0 --- /dev/null +++ b/packages/react/src/components/checkout/payment/checkout-buttons/ccavenue/ccavenue.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { useCallback } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useCheckoutContext } from '@/components/checkout/checkout'; +import { DeliveryMethods } from '@/components/checkout/delivery/delivery-method'; +import { useAuthorizeCheckout } from '@/components/checkout/payment/utils/use-authorize-checkout'; +import { PaymentProvider } from '@/components/checkout/payment/utils/use-confirm-checkout'; +import { useIsPaymentDisabled } from '@/components/checkout/payment/utils/use-is-payment-disabled'; +import { useDraftOrderShippingMethods } from '@/components/checkout/shipping/utils/use-draft-order-shipping-methods'; +import { Button } from '@/components/ui/button'; +import { useGoDaddyContext } from '@/godaddy-provider'; +import { GraphQLErrorWithCodes } from '@/lib/graphql-with-errors'; +import { cn } from '@/lib/utils'; +import { PaymentMethodType } from '@/types'; + +const CCAVENUE_PROD_URL = + 'https://secure.ccavenue.com/transaction/transaction.do?command=initiateTransaction'; +const CCAVENUE_TEST_URL = + 'https://test.ccavenue.com/transaction/transaction.do?command=initiateTransaction'; + +export function CCAvenueCheckoutButton() { + const { t, apiHost } = useGoDaddyContext(); + const { setCheckoutErrors, isConfirmingCheckout, ccavenueConfig } = + useCheckoutContext(); + const isPaymentDisabled = useIsPaymentDisabled(); + const form = useFormContext(); + const authorizeCheckout = useAuthorizeCheckout(); + + const deliveryMethod = form.watch('deliveryMethod'); + const isShipping = deliveryMethod === DeliveryMethods.SHIP; + const { data: shippingMethodsData, isLoading: isShippingMethodsLoading } = + useDraftOrderShippingMethods(); + const hasShippingMethods = (shippingMethodsData?.length ?? 0) > 0; + + // Same pattern as Square CDN in use-load-square: choose gateway URL by environment + const redirectUrl = + apiHost && !apiHost.includes('test') && !apiHost.includes('dev') + ? CCAVENUE_PROD_URL + : CCAVENUE_TEST_URL; + + const handleClick = useCallback(async () => { + const valid = await form.trigger(); + if (!valid) { + const firstError = Object.keys(form.formState.errors)[0]; + if (firstError) { + form.setFocus(firstError); + } + return; + } + + if (!ccavenueConfig?.accessCodeId) { + setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']); + return; + } + + if (isShipping && (isShippingMethodsLoading || !hasShippingMethods)) { + setCheckoutErrors(['SHIPPING_METHOD_NOT_FOUND']); + return; + } + + try { + const resData = await authorizeCheckout.mutateAsync({ + paymentType: PaymentMethodType.CCAVENUE, + paymentProvider: PaymentProvider.CCAVENUE, + paymentToken: '', + }); + const transactionRefNum = resData?.transactionRefNum ?? ''; + if (!transactionRefNum) { + setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']); + return; + } + + const formEl = document.createElement('form'); + formEl.method = 'POST'; + formEl.action = redirectUrl; + const fields: Record = { + encRequest: transactionRefNum, + access_code: ccavenueConfig.accessCodeId, + }; + Object.keys(fields).forEach(key => { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = key; + input.value = fields[key]; + formEl.appendChild(input); + }); + document.body.appendChild(formEl); + formEl.submit(); + } catch (err: unknown) { + if (err instanceof GraphQLErrorWithCodes) { + setCheckoutErrors(err.codes); + } else { + setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']); + } + } + }, [ + form, + isShipping, + isShippingMethodsLoading, + hasShippingMethods, + authorizeCheckout.mutateAsync, + setCheckoutErrors, + ccavenueConfig?.accessCodeId, + redirectUrl, + ]); + + const isBusy = isConfirmingCheckout || isPaymentDisabled; + + return ( + + ); +} diff --git a/packages/react/src/components/checkout/payment/icons/Ccavenue.tsx b/packages/react/src/components/checkout/payment/icons/Ccavenue.tsx new file mode 100644 index 00000000..29bebcbc --- /dev/null +++ b/packages/react/src/components/checkout/payment/icons/Ccavenue.tsx @@ -0,0 +1,35 @@ +export const CcavenueIcon = ({ className }: { className?: string }) => { + return ( + + CCAvenue + {/* Placeholder: replace with official CCAvenue logo SVG paths */} + + + CCAvenue + + + ); +}; + +export default CcavenueIcon; diff --git a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx index f9ad0cda..6edadc0c 100644 --- a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx +++ b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx @@ -120,6 +120,11 @@ const LazyComponents = { module => ({ default: module.PazeCheckoutButton }) ) ), + CCAvenueCheckoutButton: lazy(() => + import( + '@/components/checkout/payment/checkout-buttons/ccavenue/ccavenue' + ).then(module => ({ default: module.CCAvenueCheckoutButton })) + ), // Container Components CreditCardContainer: lazy(() => @@ -190,6 +195,11 @@ type PaymentComponentRegistry = { button: PaymentComponentKey; }; }; + [PaymentMethodType.CCAVENUE]?: { + [PaymentProvider.CCAVENUE]: { + button: PaymentComponentKey; + }; + }; }; export const lazyPaymentComponentRegistry: PaymentComponentRegistry = { @@ -244,6 +254,11 @@ export const lazyPaymentComponentRegistry: PaymentComponentRegistry = { button: 'MercadoPagoCheckoutButton', }, }, + [PaymentMethodType.CCAVENUE]: { + [PaymentProvider.CCAVENUE]: { + button: 'CCAvenueCheckoutButton', + }, + }, }; // Payment loading skeleton component diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index adb607b7..23fa3ca4 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -17,6 +17,7 @@ import { type Product, } from '@/components/checkout/line-items'; import ApplePayIcon from '@/components/checkout/payment/icons/ApplePay'; +import CcavenueIcon from '@/components/checkout/payment/icons/Ccavenue'; import GooglePayIcon from '@/components/checkout/payment/icons/GooglePay'; import MercadoPagoIcon from '@/components/checkout/payment/icons/MercadoPago'; import PayPalIcon from '@/components/checkout/payment/icons/PayPal'; @@ -69,6 +70,7 @@ const PAYMENT_METHOD_ICONS: Record = { paze: , mercadopago: , offline: , + ccavenue: , }; export function PaymentForm( @@ -118,6 +120,8 @@ export function PaymentForm( return t.payment.methods.offline; case PaymentMethodType.MERCADOPAGO: return t.payment.methods.mercadopago; + case PaymentMethodType.CCAVENUE: + return t.payment.methods.ccavenue; default: return key; } @@ -143,6 +147,8 @@ export function PaymentForm( return t.payment.descriptions?.offline; case PaymentMethodType.MERCADOPAGO: return t.payment.descriptions?.mercadopago; + case PaymentMethodType.CCAVENUE: + return t.payment.descriptions?.ccavenue; default: return undefined; } diff --git a/packages/react/src/components/checkout/payment/utils/ccavenue-return-provider.tsx b/packages/react/src/components/checkout/payment/utils/ccavenue-return-provider.tsx new file mode 100644 index 00000000..52c38169 --- /dev/null +++ b/packages/react/src/components/checkout/payment/utils/ccavenue-return-provider.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { useCheckoutContext } from '@/components/checkout/checkout'; +import { + PaymentProvider, + useConfirmCheckout, +} from '@/components/checkout/payment/utils/use-confirm-checkout'; +import { GraphQLErrorWithCodes } from '@/lib/graphql-with-errors'; + +export function CCAvenueReturnProvider({ + children, +}: { + children: React.ReactNode; +}) { + const { session, jwt, setCheckoutErrors } = useCheckoutContext(); + const confirmCheckout = useConfirmCheckout(); + const hasRun = useRef(false); + + useEffect(() => { + if (typeof window === 'undefined' || hasRun.current) return; + + const params = new URLSearchParams(window.location.search); + const encResp = params.get('encResp'); + if (!encResp) return; + + // Wait for session from context (cookie); re-run when session loads + if (!(session?.token || jwt) || !session?.id) { + return; + } + + hasRun.current = true; + + const confirmInput = { + paymentToken: encResp, + paymentType: 'ccavenue' as const, + paymentProvider: PaymentProvider.CCAVENUE, + }; + + confirmCheckout.mutateAsync(confirmInput).catch(err => { + if (err instanceof GraphQLErrorWithCodes) { + setCheckoutErrors(err.codes); + } else { + setCheckoutErrors([ + err instanceof Error ? err.message : 'Payment confirmation failed.', + ]); + } + }); + }, [session?.token, session?.id, setCheckoutErrors]); + + return <>{children}; +} diff --git a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx index 95df6409..2d381b1a 100644 --- a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx +++ b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx @@ -1,5 +1,6 @@ import { PayPalScriptProvider } from '@paypal/react-paypal-js'; import { useCheckoutContext } from '@/components/checkout/checkout'; +import { CCAvenueReturnProvider } from './ccavenue-return-provider'; import { PayPalProvider } from './paypal-provider'; import { PoyntCollectProvider } from './poynt-provider'; import { SquareProvider } from './square-provider'; @@ -17,8 +18,13 @@ interface ConditionalPaymentProvidersProps { export function ConditionalPaymentProviders({ children, }: ConditionalPaymentProvidersProps) { - const { stripeConfig, godaddyPaymentsConfig, squareConfig, paypalConfig } = - useCheckoutContext(); + const { + stripeConfig, + godaddyPaymentsConfig, + squareConfig, + paypalConfig, + ccavenueConfig, + } = useCheckoutContext(); const { payPalRequest } = useBuildPaymentRequest(); // Start with the children and conditionally wrap with providers @@ -59,6 +65,13 @@ export function ConditionalPaymentProviders({ ); } + // CCAvenue return flow: only when CCAvenue is configured (has access code) + if (ccavenueConfig?.accessCodeId?.trim()) { + wrappedChildren = ( + {wrappedChildren} + ); + } + return <>{wrappedChildren}; } diff --git a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts index 1fd76966..f83a0591 100644 --- a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts +++ b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts @@ -1,4 +1,5 @@ import { useMutation } from '@tanstack/react-query'; +import { useRef } from 'react'; import { useFormContext } from 'react-hook-form'; import { redirectToSuccessUrl, @@ -63,18 +64,15 @@ export enum PaymentProvider { CHECK_COMMERCE = 'CHECK_COMMERCE', SQUARE = 'SQUARE', OFFLINE = 'OFFLINE', + CCAVENUE = 'CCAVENUE', } export function useConfirmCheckout() { - const { - session, - jwt, - setIsConfirmingCheckout, - isConfirmingCheckout, - setCheckoutErrors, - } = useCheckoutContext(); + const { session, jwt, setIsConfirmingCheckout, setCheckoutErrors } = + useCheckoutContext(); const { apiHost } = useGoDaddyContext(); const form = useFormContext(); + const isPendingRef = useRef(false); return useMutation({ mutationFn: async ( @@ -83,7 +81,9 @@ export function useConfirmCheckout() { isExpress?: boolean; } ) => { - if (!session || !input?.paymentType || isConfirmingCheckout) return; + if (!session || !input?.paymentType) return; + if (isPendingRef.current) return; // Prevent double-calls + isPendingRef.current = true; const { isExpress, ...confirmCheckoutInput } = input; @@ -203,5 +203,8 @@ export function useConfirmCheckout() { setIsConfirmingCheckout(false); }, + onSettled: () => { + isPendingRef.current = false; + }, }); } diff --git a/packages/react/src/components/checkout/shipping/shipping-method.tsx b/packages/react/src/components/checkout/shipping/shipping-method.tsx index 1bbbdfc0..8a698c94 100644 --- a/packages/react/src/components/checkout/shipping/shipping-method.tsx +++ b/packages/react/src/components/checkout/shipping/shipping-method.tsx @@ -45,7 +45,7 @@ export function ShippingMethodForm() { const formatCurrency = useFormatCurrency(); const form = useFormContext(); const { t } = useGoDaddyContext(); - const { session } = useCheckoutContext(); + const { session, isConfirmingCheckout } = useCheckoutContext(); const updateTaxes = useUpdateTaxes(); const isPaymentDisabled = useIsPaymentDisabled(); @@ -90,7 +90,8 @@ export function ShippingMethodForm() { }); useEffect(() => { - if (isShippingMethodsLoading || isDraftOrderLoading) return; + if (isShippingMethodsLoading || isDraftOrderLoading || isConfirmingCheckout) + return; const hasShippingMethods = (shippingMethods?.length ?? 0) > 0; const currentServiceCode = shippingLines?.requestedService || null; @@ -173,6 +174,7 @@ export function ShippingMethodForm() { } } }, [ + isConfirmingCheckout, shippingMethods, shippingLines, hasShippingAddress, diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index b9a84f3a..c0c4afc2 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -9962,13 +9962,6 @@ const introspection = { name: 'UpdateMoneyInput', }, }, - { - name: 'unitPrice', - type: { - kind: 'INPUT_OBJECT', - name: 'UpdateMoneyInput', - }, - }, ], isOneOf: false, }, diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index 71464742..e3e77f19 100644 --- a/packages/react/src/lib/godaddy/checkout-mutations.ts +++ b/packages/react/src/lib/godaddy/checkout-mutations.ts @@ -81,6 +81,10 @@ export const CreateCheckoutSessionMutation = graphql(` processor checkoutTypes } + ccavenue { + processor + checkoutTypes + } express { processor checkoutTypes diff --git a/packages/react/src/lib/godaddy/checkout-queries.ts b/packages/react/src/lib/godaddy/checkout-queries.ts index 25d4b2dc..e463a8c3 100644 --- a/packages/react/src/lib/godaddy/checkout-queries.ts +++ b/packages/react/src/lib/godaddy/checkout-queries.ts @@ -81,6 +81,10 @@ export const GetCheckoutSessionQuery = graphql(` processor checkoutTypes } + ccavenue { + processor + checkoutTypes + } express { processor checkoutTypes diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index b726ff1c..999d2266 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -44,6 +44,7 @@ export const PaymentProvider = { PAZE: 'paze', OFFLINE: 'offline', MERCADOPAGO: 'mercadopago', + CCAVENUE: 'ccavenue', } as const; export const CheckoutType = { @@ -64,6 +65,7 @@ export const PaymentMethodType = { OFFLINE: 'offline', PAZE: 'paze', MERCADOPAGO: 'mercadopago', + CCAVENUE: 'ccavenue', } as const; // Union of all payment method keys