diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx
index 9ae69eb8..b345f4e3 100644
--- a/examples/nextjs/app/page.tsx
+++ b/examples/nextjs/app/page.tsx
@@ -55,7 +55,7 @@ export default async function Home() {
],
paymentMethods: {
card: {
- processor: 'godaddy',
+ processor: 'paypal',
checkoutTypes: ['standard'],
},
express: {
diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts
index 9c186fa8..a58bf364 100644
--- a/packages/localizations/src/deDe.ts
+++ b/packages/localizations/src/deDe.ts
@@ -248,6 +248,11 @@ export const deDe = {
selectState: 'Bundesland/Provinz auswählen',
selectCountry: 'Land auswählen',
enterCountry: 'Land eingeben',
+ invalidCardNumber: 'Ungültige Kartennummer',
+ invalidExpiry: 'Ungültiges Ablaufdatum',
+ invalidCvv: 'Ungültiger Sicherheitscode',
+ paymentSubmissionFailed:
+ 'Zahlung fehlgeschlagen. Bitte versuchen Sie es erneut.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Serverfehler',
diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts
index 8e3baf6f..5751044b 100644
--- a/packages/localizations/src/enIe.ts
+++ b/packages/localizations/src/enIe.ts
@@ -243,6 +243,10 @@ export const enIe = {
selectState: 'Select a county',
selectCountry: 'Select a country',
enterCountry: 'Enter a country',
+ invalidCardNumber: 'Invalid card number',
+ invalidExpiry: 'Invalid expiration date',
+ invalidCvv: 'Invalid security code',
+ paymentSubmissionFailed: 'Payment submission failed. Please try again.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Server error',
diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts
index 2458daa0..b9472309 100644
--- a/packages/localizations/src/enUs.ts
+++ b/packages/localizations/src/enUs.ts
@@ -243,6 +243,10 @@ export const enUs = {
selectState: 'Select a state/province',
selectCountry: 'Select a country',
enterCountry: 'Enter a country',
+ invalidCardNumber: 'Invalid card number',
+ invalidExpiry: 'Invalid expiration date',
+ invalidCvv: 'Invalid security code',
+ paymentSubmissionFailed: 'Payment submission failed. Please try again.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Server error',
diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts
index c55877dd..92f66711 100644
--- a/packages/localizations/src/esAr.ts
+++ b/packages/localizations/src/esAr.ts
@@ -245,6 +245,11 @@ export const esAr = {
selectState: 'Seleccioná una provincia/región',
selectCountry: 'Seleccioná un país',
enterCountry: 'Ingresá un país',
+ invalidCardNumber: 'Número de tarjeta no válido',
+ invalidExpiry: 'Fecha de vencimiento no válida',
+ invalidCvv: 'Código de seguridad no válido',
+ paymentSubmissionFailed:
+ 'Error al procesar el pago. Por favor, inténtalo de nuevo.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error del servidor',
diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts
index 100aa97b..d1438c84 100644
--- a/packages/localizations/src/esCl.ts
+++ b/packages/localizations/src/esCl.ts
@@ -247,6 +247,11 @@ export const esCl = {
selectState: 'Selecciona una región/provincia',
selectCountry: 'Selecciona un país',
enterCountry: 'Ingresa un país',
+ invalidCardNumber: 'Número de tarjeta no válido',
+ invalidExpiry: 'Fecha de vencimiento no válida',
+ invalidCvv: 'Código de seguridad no válido',
+ paymentSubmissionFailed:
+ 'Error al procesar el pago. Por favor, inténtalo de nuevo.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error del servidor',
diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts
index 5c34644b..e82930d5 100644
--- a/packages/localizations/src/esCo.ts
+++ b/packages/localizations/src/esCo.ts
@@ -245,6 +245,11 @@ export const esCo = {
selectState: 'Selecciona un departamento/región',
selectCountry: 'Selecciona un país',
enterCountry: 'Ingresa un país',
+ invalidCardNumber: 'Número de tarjeta no válido',
+ invalidExpiry: 'Fecha de vencimiento no válida',
+ invalidCvv: 'Código de seguridad no válido',
+ paymentSubmissionFailed:
+ 'Error al procesar el pago. Por favor, inténtalo de nuevo.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error del servidor',
diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts
index a2220308..51a51eec 100644
--- a/packages/localizations/src/esEs.ts
+++ b/packages/localizations/src/esEs.ts
@@ -245,6 +245,11 @@ export const esEs = {
selectState: 'Selecciona un estado/provincia',
selectCountry: 'Selecciona un país',
enterCountry: 'Introduce un país',
+ invalidCardNumber: 'Número de tarjeta no válido',
+ invalidExpiry: 'Fecha de vencimiento no válida',
+ invalidCvv: 'Código de seguridad no válido',
+ paymentSubmissionFailed:
+ 'Error al procesar el pago. Por favor, inténtalo de nuevo.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error del servidor',
diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts
index c484e103..2a34cb82 100644
--- a/packages/localizations/src/esMx.ts
+++ b/packages/localizations/src/esMx.ts
@@ -245,6 +245,11 @@ export const esMx = {
selectState: 'Seleccione un estado/provincia',
selectCountry: 'Seleccione un país',
enterCountry: 'Ingrese un país',
+ invalidCardNumber: 'Número de tarjeta no válido',
+ invalidExpiry: 'Fecha de vencimiento no válida',
+ invalidCvv: 'Código de seguridad no válido',
+ paymentSubmissionFailed:
+ 'Error al procesar el pago. Por favor, inténtalo de nuevo.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error del servidor',
diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts
index ccae27f8..9dbeca8e 100644
--- a/packages/localizations/src/esPe.ts
+++ b/packages/localizations/src/esPe.ts
@@ -245,6 +245,11 @@ export const esPe = {
selectState: 'Seleccione un departamento/región',
selectCountry: 'Seleccione un país',
enterCountry: 'Ingrese un país',
+ invalidCardNumber: 'Número de tarjeta no válido',
+ invalidExpiry: 'Fecha de vencimiento no válida',
+ invalidCvv: 'Código de seguridad no válido',
+ paymentSubmissionFailed:
+ 'Error al procesar el pago. Por favor, inténtalo de nuevo.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error del servidor',
diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts
index da390efe..ad231176 100644
--- a/packages/localizations/src/esUs.ts
+++ b/packages/localizations/src/esUs.ts
@@ -245,6 +245,11 @@ export const esUs = {
selectState: 'Seleccione un estado/provincia',
selectCountry: 'Seleccione un país',
enterCountry: 'Ingrese un país',
+ invalidCardNumber: 'Número de tarjeta no válido',
+ invalidExpiry: 'Fecha de vencimiento no válida',
+ invalidCvv: 'Código de seguridad no válido',
+ paymentSubmissionFailed:
+ 'Error al procesar el pago. Por favor, inténtalo de nuevo.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error del servidor',
diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts
index 26edeb97..1e136e32 100644
--- a/packages/localizations/src/frCa.ts
+++ b/packages/localizations/src/frCa.ts
@@ -250,6 +250,11 @@ export const frCa = {
selectState: 'Sélectionnez une province/état',
selectCountry: 'Sélectionnez un pays',
enterCountry: 'Entrez un pays',
+ invalidCardNumber: 'Numéro de carte invalide',
+ invalidExpiry: "Date d'expiration invalide",
+ invalidCvv: 'Code de sécurité invalide',
+ paymentSubmissionFailed:
+ 'Échec de la soumission du paiement. Veuillez réessayer.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Erreur de serveur',
diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts
index 13abdf5f..04b2a67c 100644
--- a/packages/localizations/src/frFr.ts
+++ b/packages/localizations/src/frFr.ts
@@ -250,6 +250,11 @@ export const frFr = {
selectState: 'Sélectionnez un état/province',
selectCountry: 'Sélectionnez un pays',
enterCountry: 'Entrez un pays',
+ invalidCardNumber: 'Numéro de carte invalide',
+ invalidExpiry: "Date d'expiration invalide",
+ invalidCvv: 'Code de sécurité invalide',
+ paymentSubmissionFailed:
+ 'Échec de la soumission du paiement. Veuillez réessayer.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Erreur du serveur',
diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts
index 280aac48..a0b2b8a6 100644
--- a/packages/localizations/src/idId.ts
+++ b/packages/localizations/src/idId.ts
@@ -243,6 +243,10 @@ export const idId = {
selectState: 'Pilih provinsi',
selectCountry: 'Pilih negara',
enterCountry: 'Masukkan negara',
+ invalidCardNumber: 'Nomor kartu tidak valid',
+ invalidExpiry: 'Tanggal kedaluwarsa tidak valid',
+ invalidCvv: 'Kode keamanan tidak valid',
+ paymentSubmissionFailed: 'Pengiriman pembayaran gagal. Silakan coba lagi.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Error server',
diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts
index 20fb884a..e6d5a3d3 100644
--- a/packages/localizations/src/itIt.ts
+++ b/packages/localizations/src/itIt.ts
@@ -251,6 +251,10 @@ export const itIt = {
selectState: 'Seleziona una regione/provincia',
selectCountry: 'Seleziona un paese',
enterCountry: 'Inserisci un paese',
+ invalidCardNumber: 'Numero di carta non valido',
+ invalidExpiry: 'Data di scadenza non valida',
+ invalidCvv: 'Codice di sicurezza non valido',
+ paymentSubmissionFailed: 'Invio del pagamento non riuscito. Riprova.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Errore del server',
diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts
index 64a36964..40dac240 100644
--- a/packages/localizations/src/ptBr.ts
+++ b/packages/localizations/src/ptBr.ts
@@ -245,6 +245,11 @@ export const ptBr = {
selectState: 'Selecione um estado/província',
selectCountry: 'Selecione um país',
enterCountry: 'Digite um país',
+ invalidCardNumber: 'Número de cartão inválido',
+ invalidExpiry: 'Data de validade inválida',
+ invalidCvv: 'Código de segurança inválido',
+ paymentSubmissionFailed:
+ 'Falha no envio do pagamento. Por favor, tente novamente.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Erro no servidor',
diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts
index 2b320271..2824a45a 100644
--- a/packages/localizations/src/qaPs.ts
+++ b/packages/localizations/src/qaPs.ts
@@ -247,6 +247,11 @@ export const qaPs = {
selectState: '[Šëlëçţ â šţâţë/þrövîñçë frôm ţhë âvâîlâblë öþţîöñš]',
selectCountry: '[Šëlëçţ â çöüñţrÿ frôm ţhë âvâîlâblë öþţîöñš]',
enterCountry: '[Ëñţër â çöüñţrÿ ñâmë fôr löçâţîöñ]',
+ invalidCardNumber: '[Invalid card number ℓσяєм]',
+ invalidExpiry: '[Invalid expiration date ℓσяєм ιρѕυм]',
+ invalidCvv: '[Invalid security code ℓσяєм]',
+ paymentSubmissionFailed:
+ '[Payment submission failed. Please try again. ℓσяєм ιρѕυм ∂σℓσя]',
},
apiErrors: {
INTERNAL_SERVER_ERROR: '[Šërvër ërrör öççürrëd]',
diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts
index 14cfdfa1..72afea53 100644
--- a/packages/localizations/src/trTr.ts
+++ b/packages/localizations/src/trTr.ts
@@ -244,6 +244,11 @@ export const trTr = {
selectState: 'Bir il/eyalet seçin',
selectCountry: 'Bir ülke seçin',
enterCountry: 'Bir ülke girin',
+ invalidCardNumber: 'Geçersiz kart numarası',
+ invalidExpiry: 'Geçersiz son kullanma tarihi',
+ invalidCvv: 'Geçersiz güvenlik kodu',
+ paymentSubmissionFailed:
+ 'Ödeme gönderimi başarısız oldu. Lütfen tekrar deneyin.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Sunucu hatası',
diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts
index 436c37ca..c1460e6c 100644
--- a/packages/localizations/src/viVn.ts
+++ b/packages/localizations/src/viVn.ts
@@ -243,6 +243,10 @@ export const viVn = {
selectState: 'Chọn tỉnh/thành phố',
selectCountry: 'Chọn quốc gia',
enterCountry: 'Nhập quốc gia',
+ invalidCardNumber: 'Số thẻ không hợp lệ',
+ invalidExpiry: 'Ngày hết hạn không hợp lệ',
+ invalidCvv: 'Mã bảo mật không hợp lệ',
+ paymentSubmissionFailed: 'Gửi thanh toán thất bại. Vui lòng thử lại.',
},
apiErrors: {
INTERNAL_SERVER_ERROR: 'Lỗi máy chủ',
diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts
index 6cf7c1d9..e71929b9 100644
--- a/packages/localizations/src/zhCn.ts
+++ b/packages/localizations/src/zhCn.ts
@@ -236,6 +236,10 @@ export const zhCn = {
selectState: '选择省份/地区',
selectCountry: '选择国家',
enterCountry: '请输入国家',
+ invalidCardNumber: '卡号无效',
+ invalidExpiry: '有效期无效',
+ invalidCvv: '安全码无效',
+ paymentSubmissionFailed: '付款提交失败。请重试。',
},
apiErrors: {
INTERNAL_SERVER_ERROR: '服务器错误',
diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts
index 1aaa6d12..286d58ff 100644
--- a/packages/localizations/src/zhSg.ts
+++ b/packages/localizations/src/zhSg.ts
@@ -236,6 +236,10 @@ export const zhSg = {
selectState: '选择州/省',
selectCountry: '选择国家',
enterCountry: '输入国家',
+ invalidCardNumber: '卡號無效',
+ invalidExpiry: '有效期無效',
+ invalidCvv: '安全碼無效',
+ paymentSubmissionFailed: '付款提交失敗。請重試。',
},
apiErrors: {
INTERNAL_SERVER_ERROR: '服务器错误',
diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/paypal.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/paypal.tsx
new file mode 100644
index 00000000..b28f95f5
--- /dev/null
+++ b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/paypal.tsx
@@ -0,0 +1,62 @@
+'use client';
+
+import { useCallback, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useCheckoutContext } from '@/components/checkout/checkout';
+import { usePayPalProvider } from '@/components/checkout/payment/utils/paypal-provider';
+import { useIsPaymentDisabled } from '@/components/checkout/payment/utils/use-is-payment-disabled';
+import { Button } from '@/components/ui/button';
+import { useGoDaddyContext } from '@/godaddy-provider';
+
+export function PayPalCreditCardCheckoutButton() {
+ const { t } = useGoDaddyContext();
+ const { isConfirmingCheckout } = useCheckoutContext();
+ const isPaymentDisabled = useIsPaymentDisabled();
+ const form = useFormContext();
+ const { cardFieldsRef, isCardFieldsReady } = usePayPalProvider();
+ const [_isProcessing, setIsProcessing] = useState(false);
+
+ const handleSubmit = useCallback(async () => {
+ try {
+ setIsProcessing(true);
+
+ // Validate the form first
+ const valid = await form.trigger();
+ if (!valid) {
+ const firstError = Object.keys(form.formState.errors)[0];
+ if (firstError) {
+ form.setFocus(firstError);
+ }
+ return;
+ }
+
+ // Check if PayPal card fields are ready
+ if (!cardFieldsRef.current || !cardFieldsRef.current.isEligible()) {
+ return;
+ }
+
+ // Submit the PayPal payment
+ await cardFieldsRef.current.submit();
+ } catch (_error) {
+ // PayPal checkout failed
+ } finally {
+ setIsProcessing(false);
+ }
+ }, [form, cardFieldsRef]);
+
+ if (!isCardFieldsReady || !cardFieldsRef.current) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/paypal/paypal.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/paypal/paypal.tsx
index 4495dbfe..d531444a 100644
--- a/packages/react/src/components/checkout/payment/checkout-buttons/paypal/paypal.tsx
+++ b/packages/react/src/components/checkout/payment/checkout-buttons/paypal/paypal.tsx
@@ -1,8 +1,4 @@
-import {
- PayPalButtons,
- PayPalScriptProvider,
- usePayPalScriptReducer,
-} from '@paypal/react-paypal-js';
+import { PayPalButtons, usePayPalScriptReducer } from '@paypal/react-paypal-js';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useCheckoutContext } from '@/components/checkout/checkout';
@@ -113,7 +109,6 @@ function PayPalButtonsWrapper() {
export function PayPalCheckoutButton() {
const { t } = useGoDaddyContext();
const { paypalConfig } = useCheckoutContext();
- const { payPalRequest } = useBuildPaymentRequest();
if (!paypalConfig) {
return (
@@ -123,22 +118,7 @@ export function PayPalCheckoutButton() {
return (
);
}
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 93dcd888..00bb6c53 100644
--- a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx
+++ b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx
@@ -33,6 +33,13 @@ const LazyComponents = {
default: module.SquareCreditCardForm,
}))
),
+ PayPalCreditCardForm: lazy(() =>
+ import(
+ '@/components/checkout/payment/payment-methods/credit-card/paypal'
+ ).then(module => ({
+ default: module.PayPalCreditCardForm,
+ }))
+ ),
// Credit Card Buttons
CreditCardCheckoutButton: lazy(() =>
@@ -56,6 +63,13 @@ const LazyComponents = {
default: module.SquareCreditCardCheckoutButton,
}))
),
+ PayPalCreditCardCheckoutButton: lazy(() =>
+ import(
+ '@/components/checkout/payment/checkout-buttons/credit-card/paypal'
+ ).then(module => ({
+ default: module.PayPalCreditCardCheckoutButton,
+ }))
+ ),
// Express Buttons
ExpressCheckoutButton: lazy(() =>
@@ -132,6 +146,10 @@ type PaymentComponentRegistry = {
form: PaymentComponentKey;
button: PaymentComponentKey;
};
+ [PaymentProvider.PAYPAL]: {
+ form: PaymentComponentKey;
+ button: PaymentComponentKey;
+ };
};
[PaymentMethodType.EXPRESS]?: {
[PaymentProvider.GODADDY]: {
@@ -177,6 +195,10 @@ export const lazyPaymentComponentRegistry: PaymentComponentRegistry = {
form: 'SquareCreditCardForm',
button: 'SquareCreditCardCheckoutButton',
},
+ [PaymentProvider.PAYPAL]: {
+ form: 'PayPalCreditCardForm',
+ button: 'PayPalCreditCardCheckoutButton',
+ },
},
[PaymentMethodType.EXPRESS]: {
[PaymentProvider.STRIPE]: {
diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/paypal.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/paypal.tsx
new file mode 100644
index 00000000..b36fc395
--- /dev/null
+++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/paypal.tsx
@@ -0,0 +1,263 @@
+'use client';
+
+import {
+ PayPalCardFieldsForm,
+ PayPalCardFieldsProvider,
+ usePayPalCardFields,
+ usePayPalScriptReducer,
+} from '@paypal/react-paypal-js';
+import { useEffect } from 'react';
+import { useCheckoutContext } from '@/components/checkout/checkout';
+import { usePayPalProvider } from '@/components/checkout/payment/utils/paypal-provider';
+import { useAuthorizeCheckout } from '@/components/checkout/payment/utils/use-authorize-checkout';
+import {
+ PaymentProvider,
+ useConfirmCheckout,
+} from '@/components/checkout/payment/utils/use-confirm-checkout';
+import { Skeleton } from '@/components/ui/skeleton';
+import { useGoDaddyContext } from '@/godaddy-provider';
+import { GraphQLErrorWithCodes } from '@/lib/graphql-with-errors';
+import { PaymentMethodType } from '@/types';
+
+function parsePayPalSubmissionError(error: any, t: any): string {
+ if (error && typeof error === 'object') {
+ if (error?.message?.includes('INVALID_NUMBER')) {
+ return t.validation.invalidCardNumber;
+ }
+ if (error?.message?.includes('INVALID_EXPIRY')) {
+ return t.validation.invalidExpiry;
+ }
+ if (error?.message?.includes('INVALID_CVV')) {
+ return t.validation.invalidCvv;
+ }
+ }
+ return t.validation.paymentSubmissionFailed;
+}
+
+function PayPalCardFieldsFormContent() {
+ const {
+ setIsCardFieldsReady,
+ setCardFieldsError,
+ cardFieldsRef,
+ fieldValidationErrors,
+ cardFieldsError,
+ } = usePayPalProvider();
+ const { cardFieldsForm } = usePayPalCardFields();
+ const [{ isResolved, isPending }] = usePayPalScriptReducer();
+ const { t } = useGoDaddyContext();
+
+ useEffect(() => {
+ if (!cardFieldsForm) return;
+
+ cardFieldsRef.current = {
+ submit: async () => {
+ try {
+ await cardFieldsForm.submit();
+ } catch (error) {
+ // Parse PayPal specific errors
+ const errorMessage = parsePayPalSubmissionError(error, t);
+ setCardFieldsError(errorMessage);
+ throw error;
+ }
+ },
+ isEligible: () => !!cardFieldsForm,
+ };
+
+ setIsCardFieldsReady(true);
+ setCardFieldsError(null);
+
+ return () => {
+ setIsCardFieldsReady(false);
+ cardFieldsRef.current = null;
+ };
+ }, [
+ cardFieldsForm,
+ setIsCardFieldsReady,
+ setCardFieldsError,
+ cardFieldsRef,
+ t,
+ ]);
+
+ if (isPending || !isResolved) {
+ return ;
+ }
+
+ return (
+
+
+
+ {/* Display field validation errors */}
+ {Object.entries(fieldValidationErrors).map(([field, error]) => (
+
+ {error}
+
+ ))}
+ {/* Display general submission errors */}
+ {cardFieldsError && (
+
+ {cardFieldsError}
+
+ )}
+
+ );
+}
+
+export function PayPalCreditCardForm() {
+ const { paypalConfig, setCheckoutErrors } = useCheckoutContext();
+ const { t } = useGoDaddyContext();
+
+ const confirmCheckout = useConfirmCheckout();
+ const authorizeCheckout = useAuthorizeCheckout();
+
+ if (!paypalConfig?.clientId) {
+ return (
+
+ {t.errors.paypalConfigMissing || 'PayPal configuration missing'}
+
+ );
+ }
+
+ return (
+ {
+ const result = await authorizeCheckout.mutateAsync({
+ paymentType: PaymentMethodType.CREDIT_CARD,
+ paymentProvider: PaymentProvider.PAYPAL,
+ });
+ return result?.id ?? '';
+ }}
+ onApprove={async data => {
+ try {
+ await confirmCheckout.mutateAsync({
+ paymentToken: data.orderID,
+ paymentType: PaymentMethodType.CREDIT_CARD,
+ paymentProvider: PaymentProvider.PAYPAL,
+ });
+ } catch (error) {
+ if (error instanceof GraphQLErrorWithCodes) {
+ setCheckoutErrors(error.codes);
+ } else {
+ setCheckoutErrors(['TRANSACTION_PROCESSING_FAILED']);
+ }
+ throw error;
+ }
+ }}
+ onError={_error => {
+ // PayPal Card Fields provider error
+ }}
+ style={{
+ '.card-field-name': {
+ border: '1px solid oklch(0.9 0.025 245)',
+ // @ts-ignore
+ 'border-radius': '0.375rem',
+ 'background-color': 'oklch(1 0 0)',
+ padding: '0.5rem 0.75rem',
+ 'font-size': '0.875rem',
+ 'line-height': '1.25rem',
+ height: '3rem',
+ color: 'oklch(0.13 0 0)',
+ },
+ '.card-field-number': {
+ // @ts-ignore
+ border: '1px solid oklch(0.9 0.025 245) !important',
+ // @ts-ignore
+ 'border-radius': '0.375rem',
+ 'background-color': 'oklch(1 0 0)',
+ padding: '0.5rem 0.75rem',
+ 'font-size': '0.875rem',
+ 'line-height': '1.25rem',
+ height: '3rem',
+ color: 'oklch(0.13 0 0)',
+ },
+ '.card-field-expiry': {
+ // @ts-ignore
+ border: '1px solid oklch(0.9 0.025 245)',
+ // @ts-ignore
+ 'border-radius': '0.375rem',
+ 'background-color': 'oklch(1 0 0)',
+ padding: '0.5rem 0.75rem',
+ 'font-size': '0.875rem',
+ 'line-height': '1.25rem',
+ height: '3rem',
+ color: 'oklch(0.13 0 0)',
+ },
+ '.card-field-cvv': {
+ border: '1px solid oklch(0.9 0.025 245)',
+ // @ts-ignore
+ 'border-radius': '0.375rem',
+ 'background-color': 'oklch(1 0 0)',
+ padding: '0.5rem 0.75rem',
+ 'font-size': '0.875rem',
+ 'line-height': '1.25rem',
+ height: '3rem',
+ color: 'oklch(0.13 0 0)',
+ },
+ '.card-field-name:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '.card-field-number:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '.card-field-expiry:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '.card-field-cvv:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '.card-field-name.invalid': {
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ border: '1px solid oklch(0.577 0.245 27.325) !important',
+ },
+ '.card-field-number.invalid': {
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ border: '1px solid oklch(0.577 0.245 27.325) !important',
+ },
+ '.card-field-expiry.invalid': {
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ border: '1px solid oklch(0.577 0.245 27.325) !important',
+ },
+ '.card-field-cvv.invalid': {
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ border: '1px solid oklch(0.577 0.245 27.325) !important',
+ },
+ '.card-field-name.invalid:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '.card-field-number.invalid:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '.card-field-expiry.invalid:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '.card-field-cvv.invalid:focus': {
+ outline: 'none',
+ // @ts-ignore
+ 'box-shadow': 'unset',
+ },
+ '::placeholder': {
+ color: 'oklch(0.556 0 0)',
+ },
+ }}
+ >
+
+
+ );
+}
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 8194087b..95df6409 100644
--- a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx
+++ b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx
@@ -1,7 +1,10 @@
+import { PayPalScriptProvider } from '@paypal/react-paypal-js';
import { useCheckoutContext } from '@/components/checkout/checkout';
+import { PayPalProvider } from './paypal-provider';
import { PoyntCollectProvider } from './poynt-provider';
import { SquareProvider } from './square-provider';
import { StripeProvider } from './stripe-provider';
+import { useBuildPaymentRequest } from './use-build-payment-request';
interface ConditionalPaymentProvidersProps {
children: React.ReactNode;
@@ -14,8 +17,9 @@ interface ConditionalPaymentProvidersProps {
export function ConditionalPaymentProviders({
children,
}: ConditionalPaymentProvidersProps) {
- const { stripeConfig, godaddyPaymentsConfig, squareConfig } =
+ const { stripeConfig, godaddyPaymentsConfig, squareConfig, paypalConfig } =
useCheckoutContext();
+ const { payPalRequest } = useBuildPaymentRequest();
// Start with the children and conditionally wrap with providers
let wrappedChildren = children;
@@ -37,6 +41,24 @@ export function ConditionalPaymentProviders({
wrappedChildren = {wrappedChildren};
}
+ // Only wrap with PayPal providers if PayPal is configured
+ if (paypalConfig?.clientId?.trim()) {
+ wrappedChildren = (
+
+ {wrappedChildren}
+
+ );
+ }
+
return <>{wrappedChildren}>;
}
diff --git a/packages/react/src/components/checkout/payment/utils/paypal-provider.tsx b/packages/react/src/components/checkout/payment/utils/paypal-provider.tsx
new file mode 100644
index 00000000..6e30adfd
--- /dev/null
+++ b/packages/react/src/components/checkout/payment/utils/paypal-provider.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import type React from 'react';
+import {
+ createContext,
+ type ReactNode,
+ useContext,
+ useRef,
+ useState,
+} from 'react';
+
+interface PayPalCardFieldsRef {
+ submit: () => Promise;
+ isEligible: () => boolean;
+}
+
+interface PayPalProviderContextValue {
+ cardFieldsRef: React.MutableRefObject;
+ isCardFieldsReady: boolean;
+ setIsCardFieldsReady: (ready: boolean) => void;
+ cardFieldsError: string | null;
+ setCardFieldsError: (error: string | null) => void;
+ fieldValidationErrors: Record;
+ setFieldValidationErrors: (errors: Record) => void;
+}
+
+const PayPalProviderContext = createContext(
+ null
+);
+
+interface PayPalProviderProps {
+ children: ReactNode;
+}
+
+export function PayPalProvider({ children }: PayPalProviderProps) {
+ const cardFieldsRef = useRef(null);
+ const [isCardFieldsReady, setIsCardFieldsReady] = useState(false);
+ const [cardFieldsError, setCardFieldsError] = useState(null);
+ const [fieldValidationErrors, setFieldValidationErrors] = useState<
+ Record
+ >({});
+
+ const value = {
+ cardFieldsRef,
+ isCardFieldsReady,
+ setIsCardFieldsReady,
+ cardFieldsError,
+ setCardFieldsError,
+ fieldValidationErrors,
+ setFieldValidationErrors,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePayPalProvider() {
+ const context = useContext(PayPalProviderContext);
+ if (!context) {
+ throw new Error('usePayPalProvider must be used within a PayPalProvider');
+ }
+ return context;
+}
diff --git a/packages/react/src/components/checkout/payment/utils/use-authorize-checkout.ts b/packages/react/src/components/checkout/payment/utils/use-authorize-checkout.ts
new file mode 100644
index 00000000..86eccf05
--- /dev/null
+++ b/packages/react/src/components/checkout/payment/utils/use-authorize-checkout.ts
@@ -0,0 +1,20 @@
+import { useMutation } from '@tanstack/react-query';
+import { useCheckoutContext } from '@/components/checkout/checkout';
+import { useGoDaddyContext } from '@/godaddy-provider';
+import { authorizeCheckoutSession } from '@/lib/godaddy/godaddy';
+import type { AuthorizeCheckoutSessionInput } from '@/types';
+
+export function useAuthorizeCheckout() {
+ const { session, jwt } = useCheckoutContext();
+ const { apiHost } = useGoDaddyContext();
+
+ return useMutation({
+ mutationFn: async (input: AuthorizeCheckoutSessionInput['input']) => {
+ const result = jwt
+ ? await authorizeCheckoutSession(input, { accessToken: jwt }, apiHost)
+ : await authorizeCheckoutSession(input, session, apiHost);
+
+ return result.authorizeCheckoutSession;
+ },
+ });
+}
diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts
index 024dbb84..228affb3 100644
--- a/packages/react/src/lib/godaddy/checkout-env.ts
+++ b/packages/react/src/lib/godaddy/checkout-env.ts
@@ -6391,6 +6391,26 @@ const introspection = {
],
isDeprecated: false,
},
+ {
+ name: 'authorizeCheckoutSession',
+ type: {
+ kind: 'OBJECT',
+ name: 'CheckoutSession',
+ },
+ args: [
+ {
+ name: 'input',
+ type: {
+ kind: 'NON_NULL',
+ ofType: {
+ kind: 'INPUT_OBJECT',
+ name: 'MutationAuthorizeCheckoutSessionInput',
+ },
+ },
+ },
+ ],
+ isDeprecated: false,
+ },
{
name: 'calculateCheckoutSessionTaxes',
type: {
@@ -6670,6 +6690,40 @@ const introspection = {
],
isOneOf: false,
},
+ {
+ kind: 'INPUT_OBJECT',
+ name: 'MutationAuthorizeCheckoutSessionInput',
+ inputFields: [
+ {
+ name: 'paymentProvider',
+ type: {
+ kind: 'NON_NULL',
+ ofType: {
+ kind: 'SCALAR',
+ name: 'String',
+ },
+ },
+ },
+ {
+ name: 'paymentToken',
+ type: {
+ kind: 'SCALAR',
+ name: 'String',
+ },
+ },
+ {
+ name: 'paymentType',
+ type: {
+ kind: 'NON_NULL',
+ ofType: {
+ kind: 'SCALAR',
+ name: 'String',
+ },
+ },
+ },
+ ],
+ isOneOf: false,
+ },
{
kind: 'INPUT_OBJECT',
name: 'MutationConfirmCheckoutSessionInput',
diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts
index e6778eab..07846812 100644
--- a/packages/react/src/lib/godaddy/checkout-mutations.ts
+++ b/packages/react/src/lib/godaddy/checkout-mutations.ts
@@ -447,3 +447,12 @@ export const RefreshCheckoutTokenMutation = graphql(`
}
}
`);
+
+export const AuthorizeCheckoutSessionMutation = graphql(`
+ mutation AuthorizeCheckoutSession($input: MutationAuthorizeCheckoutSessionInput!) {
+ authorizeCheckoutSession(input: $input) {
+ id
+ token
+ }
+ }
+`);
diff --git a/packages/react/src/lib/godaddy/godaddy.ts b/packages/react/src/lib/godaddy/godaddy.ts
index 5d45662b..5af1b86d 100644
--- a/packages/react/src/lib/godaddy/godaddy.ts
+++ b/packages/react/src/lib/godaddy/godaddy.ts
@@ -24,6 +24,7 @@ import type {
ApplyCheckoutSessionFulfillmentLocationInput,
ApplyCheckoutSessionShippingMethodInput,
ApplyDiscountCodesInput,
+ AuthorizeCheckoutSessionInput,
CheckoutSession,
CheckoutSessionInput,
ConfirmCheckoutMutationInput,
@@ -44,6 +45,7 @@ import {
ApplyCheckoutSessionDiscountMutation,
ApplyCheckoutSessionFulfillmentLocationMutation,
ApplyCheckoutSessionShippingMethodMutation,
+ AuthorizeCheckoutSessionMutation,
CalculateCheckoutSessionTaxesMutation,
ConfirmCheckoutSessionMutation,
CreateCheckoutSessionMutation,
@@ -853,6 +855,62 @@ export function confirmCheckout(
);
}
+export function authorizeCheckoutSession(
+ input: AuthorizeCheckoutSessionInput['input'],
+ session: CheckoutSession | undefined | null,
+ apiHost?: string
+): Promise>;
+export function authorizeCheckoutSession(
+ input: AuthorizeCheckoutSessionInput['input'],
+ auth: { accessToken: string | undefined },
+ apiHost?: string
+): Promise>;
+export function authorizeCheckoutSession(
+ input: AuthorizeCheckoutSessionInput['input'],
+ sessionOrAuth:
+ | CheckoutSession
+ | undefined
+ | null
+ | { accessToken: string | undefined },
+ apiHost?: string
+) {
+ const GODADDY_HOST = getHostByEnvironment(apiHost);
+
+ if (sessionOrAuth && 'accessToken' in sessionOrAuth) {
+ if (!sessionOrAuth.accessToken) {
+ throw new Error('No access token provided');
+ }
+ return graphqlRequestWithErrors<
+ ResultOf
+ >(
+ GODADDY_HOST,
+ AuthorizeCheckoutSessionMutation,
+ { input },
+ {
+ Authorization: `Bearer ${sessionOrAuth.accessToken}`,
+ }
+ );
+ }
+
+ const session = sessionOrAuth;
+ if (!session?.token || !session?.id) {
+ throw new Error('No session token or ID provided');
+ }
+
+ return graphqlRequestWithErrors<
+ ResultOf
+ >(
+ GODADDY_HOST,
+ AuthorizeCheckoutSessionMutation,
+ { input },
+ {
+ 'x-session-token': `${session.token}`,
+ 'x-session-id': session.id,
+ 'x-store-id': session.storeId,
+ }
+ );
+}
+
export function getDraftOrderShippingMethods(
session: CheckoutSession | undefined | null,
destination?: GetCheckoutSessionShippingRatesInput['destination'],
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index d4b06da6..2cfc2edd 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -9,6 +9,7 @@ import type {
ApplyCheckoutSessionDiscountMutation,
ApplyCheckoutSessionFulfillmentLocationMutation,
ApplyCheckoutSessionShippingMethodMutation,
+ AuthorizeCheckoutSessionMutation,
CalculateCheckoutSessionTaxesMutation,
ConfirmCheckoutSessionMutation,
CreateCheckoutSessionMutation,
@@ -160,6 +161,10 @@ export type ConfirmCheckoutMutationInput = VariablesOf<
typeof ConfirmCheckoutSessionMutation
>;
+export type AuthorizeCheckoutSessionInput = VariablesOf<
+ typeof AuthorizeCheckoutSessionMutation
+>;
+
export type GetCheckoutSessionShippingRatesInput = VariablesOf<
typeof DraftOrderShippingRatesQuery
>;