在 Next.js 13 App Router TypeScript 项目中集成 Stripe 支付元素

问题描述 投票:0回答:1

我正在尝试使用 TypeScript 将 Stripe Payment Element 集成到我的 Next.js 13 App Router 项目中。我的项目是用“app”目录组织的。我已经浏览了 Stripe 网站和 GitHub 存储库的各种选项,但遇到了困难。每次尝试只会显示“卡号”、“MM / YY”、“CVC”和“卡付款”等文本,而不是字段和按钮等实际 UI 元素。

我尝试遵循 Stripe 文档中提供的示例,特别是来自 https://stripe.com/docs/ payments/ payment-element https://stripe.com/docs/ payments/ 的代码快速入门?客户端=下一步。最初,我尝试了 .js 格式的代码,但没有成功。然后我将其转换为 .tsx 和 .ts,但问题仍然存在。删除与 /api 和密钥相关的逻辑后,UI 元素最终显示,但没有样式或表单字段。

这是我当前的条纹形式。

stripe payment form

我遇到了与组件中的 options 对象相关的错误,这导致我删除了 options={options} 属性。这可能是导致问题的原因之一。我也尝试过使用基于人工智能的解决方案,但没有成功。

以下是我最近尝试的相关代码片段:

page.tsx:

'use client';

import React, { useEffect, useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";

import CheckoutForm from "../components/CheckoutForm";

// Make sure to call loadStripe outside of a component’s render to avoid
// recreating the Stripe object on every render.
// This is your test publishable API key.
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

export default function App() {
  const [clientSecret, setClientSecret] = useState<string>("");

  useEffect(() => {
    // Create PaymentIntent as soon as the page loads
    fetch("/api/create-payment-intent", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ items: [{ id: "xl-tshirt" }] }),
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, []);

  const appearance = {
    theme: 'stripe',
  };
  
  const options = {
    clientSecret,
    appearance,
  };

  return (
    <div className="App">
      {clientSecret && (
        <Elements stripe={stripePromise}>
          <CheckoutForm />
        </Elements>
      )}
    </div>
  );
}

CheckoutForm.tsx:

import React, { useEffect, useState } from "react";
import { StripePaymentElementOptions, StripeLinkAuthenticationElementChangeEvent } from "@stripe/stripe-js";
import {
  PaymentElement,
  LinkAuthenticationElement,
  useStripe,
  useElements
} from "@stripe/react-stripe-js";

export default function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  const [email, setEmail] = useState<string>('');
  const [message, setMessage] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);

  useEffect(() => {
    if (!stripe) {
      return;
    }

    const clientSecret = new URLSearchParams(window.location.search).get(
      "payment_intent_client_secret"
    );

    if (!clientSecret) {
      return;
    }

    stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
      if (!paymentIntent) {
        return;
      }
      switch (paymentIntent.status) {
        case "succeeded":
          setMessage("Payment succeeded!");
          break;
        case "processing":
          setMessage("Your payment is processing.");
          break;
        case "requires_payment_method":
          setMessage("Your payment was not successful, please try again.");
          break;
        default:
          setMessage("Something went wrong.");
          break;
      }
    });
  }, [stripe]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!stripe || !elements) {
      // Stripe.js hasn't yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }

    setIsLoading(true);

    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        // Make sure to change this to your payment completion page
        return_url: "http://localhost:3000",
      },
    });

    // This point will only be reached if there is an immediate error when
    // confirming the payment. Otherwise, your customer will be redirected to
    // your `return_url`. For some payment methods like iDEAL, your customer will
    // be redirected to an intermediate site first to authorize the payment, then
    // redirected to the `return_url`.
    if (error && (error.type === "card_error" || error.type === "validation_error")) {
      setMessage(error.message || ''); // Use an empty string as a default value
    } else {
      setMessage("An unexpected error occurred.");
    }

    setIsLoading(false);
  };

  const paymentElementOptions: StripePaymentElementOptions = {
    layout: "tabs",
  };

  return (
    <form id="payment-form" onSubmit={handleSubmit}>
      <LinkAuthenticationElement
        id="link-authentication-element"
        onChange={(e: StripeLinkAuthenticationElementChangeEvent) => setEmail(e.value!.toString())}
      />
      <PaymentElement id="payment-element" options={paymentElementOptions} />
      <button disabled={isLoading || !stripe || !elements} id="submit">
        <span id="button-text">
          {isLoading ? <div className="spinner" id="spinner"></div> : "Pay now"}
        </span>
      </button>
      {/* Show any error or success messages */}
      {message && <div id="payment-message">{message}</div>}
    </form>
  );
}

/api/create- payment-intent/route.ts:

import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";

// This is your test secret API key.
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2023-08-16", // Specify your preferred API version
});

const calculateOrderAmount = (items: any[]): number => {
  // Replace this function with a calculation of the order's amount
  // Calculate the order total on the server to prevent
  // people from directly manipulating the amount on the client
  return 1400;
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    const { items } = req.body;

    // Create a PaymentIntent with the order amount and currency
    const paymentIntent = await stripe.paymentIntents.create({
      amount: calculateOrderAmount(items),
      currency: "usd",
      // In the latest version of the API, specifying the `automatic_payment_methods` parameter is optional because Stripe enables its functionality by default.
      automatic_payment_methods: {
        enabled: true,
      },
    });

    res.send({
      clientSecret: paymentIntent.client_secret,
    });
  } catch (error) {
    console.error("Error creating PaymentIntent:", error);
    res.status(500).json({ error: "An error occurred while processing your request." });
  }
}

非常感谢您为解决此问题提供的任何帮助。 UI 元素似乎没有按预期呈现,并且我不确定如何正确配置组件内的选项对象。预先感谢您的帮助!

typescript element payment next.js13
1个回答
0
投票

您可以使用 serverActions 将处理程序替换为函数

由于状态和 onChange 处理程序的原因,您可能需要在结帐表单中使用“使用客户端”指令。

您还可以使用'@stripe/stripe-js'包中的StripeElements,与stripe React包相比,它提供了更灵活的API

    const stripeElements = useRef<StripeElements>(null) 

    const FormVisible = async () => {
    
    const stripe = await getStripe();
    const response = await fetch('/api/stripe/create-payment-intent', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(NotImportantBodyToQuestionContext),
    });
    //get client secret, it stored in payment intent
    const {clientSecret} = await response.json();


    const appearance = {
        theme: 'stripe',
    };
    
    stripeElements.current = stripe!.elements({appearance, clientSecret});

    const paymentElementOptions = {
        layout: 'tabs',
    };

    // @ts-ignore
    const paymentElement = stripeElements.current.create('payment', paymentElementOptions);
    paymentElement.mount('#payment-element'); //or any other id
    setloader(false)
}

希望对你有帮助,有问题欢迎提问

© www.soinside.com 2019 - 2024. All rights reserved.