Browse Source

paypal 支付 -- 调试

fogwind 1 ngày trước cách đây
mục cha
commit
f4bb1945e5

+ 67 - 0
src/app/api/shop/addProductInCart/route.ts

@@ -0,0 +1,67 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+import { data, p } from "framer-motion/client";
+
+export async function POST(req: NextRequest) {
+    try {
+        const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+        const params = await req.json();
+        const response = await restApiFetch<{
+            data: any;
+            variables: {
+                productId: number;
+                quantity: number;
+                variantId: number;
+            }
+        }>({
+            api: '/add-product-in-cart',
+            method:'POST',
+            cache:'no-store',
+            variables: params,
+            // variables: {
+            //     productId: 1,
+            //     quantity: 1,
+            //     // variantId: 111
+            // },
+            guestToken: authorizationToken,
+        });
+
+        /**
+         * 
+         * {
+                "status": 400,
+                "data": {
+                    "type": "/errors/400",
+                    "title": "Bad Request",
+                    "status": 400,
+                    "detail": "bagistoapi::app.graphql.cart.product-id-required"
+                }
+            }
+         */
+        return NextResponse.json({
+            status: response.status,
+            data: response.body,
+        });
+    } catch (error) {
+        console.log('add-product-in-cart error --- ', error);
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 40 - 0
src/app/api/shop/categories/[id]/route.ts

@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest,{ params }: { params: Promise<{ id: string }> }) {
+    try {
+        const { id } = await params;
+        const guestToken = getAuthToken(req);
+        const response = await restApiFetch<any>({
+            api: `/categories/${id}`,
+            method:'GET',
+            cache:'no-store',
+            guestToken,
+        });
+
+        return NextResponse.json({
+            status: response.status,
+            data: response.body,
+        });
+    } catch (error) {
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 42 - 0
src/app/api/shop/categories/route.ts

@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest) {
+    try {
+        const { searchParams } = req.nextUrl;
+        const queryString = searchParams.toString();
+        const guestToken = getAuthToken(req);
+        const api = queryString ? `/categories?${queryString}` : `/categories`;
+        const response = await restApiFetch<any>({
+            api: api,
+            method:'GET',
+            cache:'no-store',
+            guestToken,
+        });
+
+        return NextResponse.json({
+            status: response.status,
+            data: response.body,
+        });
+    } catch (error) {
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 41 - 0
src/app/api/shop/categoryTrees/route.ts

@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest) {
+    try {
+     
+        const guestToken = getAuthToken(req); // 获取headers中的Authorization的值
+        
+        const response = await restApiFetch<any>({
+            api: '/category-trees',
+            method:'GET',
+            cache:'no-store',
+            guestToken,
+        });
+
+        return NextResponse.json({
+            status: response.status,
+            data: response.body,
+        });
+    } catch (error) {
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 59 - 0
src/app/api/shop/paypal/smart-button/capture-order/route.ts

@@ -0,0 +1,59 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function POST(req: NextRequest) {
+    try {
+        const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+        const params = await req.json();
+        const response = await restApiFetch<{
+            data: any;
+            variables: {
+                orderId: string;
+            }
+        }>({
+            api: '/paypal/smart-button/capture-order',
+            method:'POST',
+            cache:'no-store',
+            variables: params,
+            guestToken: authorizationToken,
+        });
+
+        /**
+         * 
+         * {
+                "status": 400,
+                "data": {
+                    "type": "/errors/400",
+                    "title": "Bad Request",
+                    "status": 400,
+                    "detail": "bagistoapi::app.graphql.cart.product-id-required"
+                }
+            }
+         */
+        return NextResponse.json({
+            status: response.status,
+            data: response.body,
+        });
+    } catch (error) {
+        console.log('add-product-in-cart error --- ', error);
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 18 - 4
src/app/layout.tsx

@@ -7,6 +7,7 @@ import { SpeculationRules } from "@components/theme/SpeculationRules";
 import { ErrorBoundary } from "@/components/error/ErrorBoundary";
 import { AddToCartModalWrapper } from "@components/common/AddToCartModal/AddToCartModalWrapper";
 import clsx from "clsx";
+import { PayPalProvider } from "@paypal/react-paypal-js/sdk-v6";
 
 // Locale revision marker — required for SSR a11y locale sync (Next.js i18n).
 // Removing this breaks server-rendered locale negotiation. Do not edit.
@@ -51,10 +52,23 @@ export default function RootLayout({
       )}>
         <main>
           <ErrorBoundary>
-            <GlobalProviders>
-              {children}
-              <AddToCartModalWrapper />
-            </GlobalProviders>
+            <PayPalProvider
+              clientId="AcI7EUfRlGbMDeK94cJvJ46Iu14Z-Ms1_pQRxABQe6lMdt03eaL42BuGocIDIBnm-9F9IpxUke0CazrZ"
+              components={[
+                "paypal-payments",
+                "venmo-payments",
+                "paypal-guest-payments",
+                "paypal-subscriptions",
+                "card-fields",
+                "paypal-messages",
+              ]}
+              pageType="checkout"
+            >
+              <GlobalProviders>
+                {children}
+                <AddToCartModalWrapper />
+              </GlobalProviders>
+            </PayPalProvider>
             <SpeculationRules />
           </ErrorBoundary>
         </main>

+ 1 - 0
src/components/checkout/stepper/index.tsx

@@ -91,6 +91,7 @@ export default function Stepper(
         href: "/checkout?step=payment",
         component: (
           <Review
+            selectedPayment={selectedPayment}
             billingAddress={billingAddress}
             selectedPaymentTitle={selectedPaymentTitle}
             selectedShippingRate={selectedShippingRate}

+ 115 - 2
src/components/checkout/stepper/review/OrderReview.tsx

@@ -1,4 +1,6 @@
 "use client";
+
+import { useState } from "react";
 import { useForm } from "react-hook-form";
 import {
   AddressDataTypes,
@@ -6,25 +8,129 @@ import {
 import { isObject } from "@/utils/type-guards";
 import { useCheckout } from "@utils/hooks/useCheckout";
 import { ProceedToCheckout } from "../ProceedToCheckout";
+
+import {
+  usePayPal,
+  useEligibleMethods,
+  INSTANCE_LOADING_STATE,
+  PayPalOneTimePaymentButton,
+  VenmoOneTimePaymentButton,
+  PayLaterOneTimePaymentButton,
+  PayPalGuestPaymentButton,
+  PayPalCreditOneTimePaymentButton,
+  type OnApproveDataOneTimePayments,
+  type OnErrorData,
+  type OnCompleteData,
+  type OnCancelDataOneTimePayments,
+} from "@paypal/react-paypal-js/sdk-v6";
+
+import { useDispatch } from "react-redux";
+import {clearCart} from "@/store/slices/cart-slice";
+import { getCookie, setCookie } from "@/utils/cookie-tools";
+import { clientFetch } from "@/lib/restApiClient";
+import { ORDER_ID } from "@utils/constants";
+import { useRouter } from "next/navigation";
+
+type ModalType = "success" | "cancel" | "error" | null;
 export default function OrderReview({
+  selectedPayment,
   selectedPaymentTitle,
   shippingAddress,
   billingAddress,
   selectedShipping : _selectedShipping,
   selectedShippingRateTitle,
 }: {
+  selectedPayment?: any;
   selectedPaymentTitle?: string;
   shippingAddress?: AddressDataTypes;
   billingAddress?: AddressDataTypes;
   selectedShipping?: string;
   selectedShippingRateTitle?: string;
 }) {
-  const { isPlaceOrder, savePlaceOrder } = useCheckout();
+  const dispatch = useDispatch();
+  const router = useRouter();
+
+  const { isPlaceOrder, savePlaceOrder, createOrder } = useCheckout();
   const { handleSubmit } = useForm();
   const onSubmit = () => {
     savePlaceOrder();
   };
 
+  const [modalState, setModalState] = useState<ModalType>(null);
+  const { loadingStatus } = usePayPal();
+
+  // Fetch eligibility for one-time payment flow
+  const {
+    error: eligibilityError,
+    eligiblePaymentMethods,
+    isLoading: isEligibilityLoading,
+  } = useEligibleMethods({
+    payload: {
+      currencyCode: "USD",
+      paymentFlow: "ONE_TIME_PAYMENT",
+    },
+  });
+  const isLoading = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
+
+  let orderId = '';
+  const handleCreateOrder = async () => {
+    let res = await createOrder();
+    console.log("createOrder res ---- ", res);
+    orderId = res?.orderId || '';
+    return res;
+  };
+
+  const handlePaymentCallbacks = {
+    onApprove: async (data: OnApproveDataOneTimePayments) => {
+      console.log("Payment approved:", data);
+      // api/shop/paypal/smart-button/capture-order
+      // const captureResult = await captureOrder({ orderId: data.orderId });
+      const captureResult = await clientFetch('/api/shop/paypal/smart-button/capture-order',{ 
+        method: "POST",
+        body: JSON.stringify({ orderId: data.orderId }) 
+      });
+      console.log("Payment capture result:", captureResult);
+
+      // setModalState("success");
+      // setCookie(ORDER_ID, orderId);
+      // dispatch(clearCart());
+      // router.replace("/success");
+    },
+
+    onCancel: (data: OnCancelDataOneTimePayments) => {
+      console.log("Payment cancelled:", data);
+      setModalState("cancel");
+    },
+
+    onError: (data: OnErrorData) => {
+      console.error("Payment error:", data);
+      setModalState("error");
+    },
+
+    onComplete: (data: OnCompleteData) => {
+      console.log("Payment session completed");
+      console.log("On Complete data:", data);
+    },
+  };
+
+
+  const paymentButtons = isLoading ? (
+    <div style={{ padding: "1rem", textAlign: "center" }}>
+      Loading payment methods...
+    </div>
+  ) : eligibilityError ? (
+    <div style={{ padding: "1rem", textAlign: "center", color: "red" }}>
+      Failed to load payment options. Please refresh the page.
+    </div>
+  ) : (
+      <PayPalOneTimePaymentButton
+        createOrder={handleCreateOrder}
+        presentationMode="auto"
+        {...handlePaymentCallbacks}
+      />
+  );
+
+
   return (
     <div className="mt-4 flex-col mb-20 sm:mb-0">
       <div className="relative">
@@ -87,7 +193,10 @@ export default function OrderReview({
         )}
       </div>
       <div className="flex flex-col gap-6">
-        <form onSubmit={handleSubmit(onSubmit)}>
+       {selectedPayment === 'paypal_smart_button'
+       
+       ? paymentButtons
+       : <form onSubmit={handleSubmit(onSubmit)}>
           <div className="justify-self-end">
             <ProceedToCheckout
               buttonName="Place Order"
@@ -95,6 +204,10 @@ export default function OrderReview({
             />
           </div>
         </form>
+      }
+
+
+        
       </div>
     </div>
   );

+ 3 - 0
src/components/checkout/stepper/review/index.tsx

@@ -4,12 +4,14 @@ import OrderReview from "./OrderReview";
 
 export const Review: FC<{
   selectedPaymentTitle?: string;
+  selectedPayment?: any;
   shippingAddress?: AddressDataTypes;
   billingAddress?: AddressDataTypes;
   selectedShippingRate?: string;
   selectedShippingRateTitle?: string;
 }> = ({
   selectedPaymentTitle,
+  selectedPayment,
   shippingAddress,
   billingAddress,
   selectedShippingRate,
@@ -18,6 +20,7 @@ export const Review: FC<{
   return (
     <OrderReview
       billingAddress={billingAddress}
+      selectedPayment={selectedPayment}
       selectedPaymentTitle={selectedPaymentTitle}
       selectedShipping={selectedShippingRate}
       selectedShippingRateTitle={selectedShippingRateTitle}

+ 1 - 0
src/graphql/checkout/mutations/CreateCheckoutOrder.ts

@@ -10,6 +10,7 @@ export const CREATE_CHECKOUT_ORDER = gql`
         orderId
         message
         success
+        paymentTransactionId
       }
     }
   }

+ 1 - 0
src/types/checkout/type.ts

@@ -151,6 +151,7 @@ export interface CheckoutOrder {
   orderId: string;
   message: string;
   success: boolean;
+  paymentTransactionId: string;
 }
 export interface CreateCheckoutOrderPayload {
   checkoutOrder: CheckoutOrder;

+ 21 - 2
src/utils/hooks/useCheckout.ts

@@ -1,4 +1,5 @@
-import { useMutation } from "@apollo/client/react";
+"use client";
+import { useMutation,useApolloClient } from "@apollo/client/react";
 import { useRouter } from "next/navigation";
 import { useCustomToast } from "./useToast";
 import { useDispatch } from "react-redux";
@@ -18,7 +19,11 @@ import {
   CREATE_CHECKOUT_SHIPPING_METHODS,
   GET_CHECKOUT_ADDRESSES,
 } from "@/graphql";
-import { CreateCheckoutShippingMethodData, CreateCheckoutPaymentMethodResponse, CreateCheckoutOrderData } from "@/types/checkout/type";
+import { 
+  CreateCheckoutShippingMethodData, 
+  CreateCheckoutPaymentMethodResponse, 
+  CreateCheckoutOrderData,
+} from "@/types/checkout/type";
 
 export const useCheckout = () => {
   const router = useRouter();
@@ -26,6 +31,7 @@ export const useCheckout = () => {
   const { showToast } = useCustomToast();
   const dispatch = useDispatch();
   const { getCartDetail } = useCartDetail();
+  const client = useApolloClient();
 
   const handleMutationError = (error: any) => {
     showToast(error?.message || "An error occurred", "danger");
@@ -135,6 +141,18 @@ export const useCheckout = () => {
       onError: handleMutationError,
     },
   );
+  const createOrder = async () => {
+    const token = getCartToken();
+    let response = await client.mutate<CreateCheckoutOrderData>({
+      mutation: CREATE_CHECKOUT_ORDER,
+      variables: {
+        token: token || "",
+      },
+    });
+    let orderId = response?.data?.createCheckoutOrder?.checkoutOrder?.paymentTransactionId || ""; 
+    console.log("createOrder response ---- ", response);
+    return { orderId: orderId };
+  };
 
   const savePlaceOrder = async () => {
     const token = getCartToken();
@@ -159,5 +177,6 @@ export const useCheckout = () => {
     saveCheckoutPayment,
     isPlaceOrder,
     savePlaceOrder,
+    createOrder
   };
 };