有人知道我们如何定制 Stripe 支付布局吗?我的目标是更改布局,例如第一个字段应为全宽,其余 3 个字段应对齐。
来自 stripe 的样式和 iframe https://js.stripe.com/v3/
这不是您在使用 PaymentElement 时可以控制的。通过这种集成,Stripe 控制 UI,但不允许您控制内部的确切布局。您可以控制父级的宽度/高度并通过各种选项进行样式设置,但没有一个可以通过这种方式控制每个字段。
需要注意的是,虽然您的屏幕截图仅显示卡详细信息,但 Stripe 的 PaymentElement 针对收集各种付款方式类型的付款方式详细信息进行了优化。沿着流程,预先收集的信息量各不相同,例如全页重定向到合作伙伴与同步成功或失败。
如果您确实想控制布局本身并且只关注卡支付,您可以使用不同的集成,为每条信息(号码、到期日期和 CVC)创建元素,并使用您自己的信息在页面上显示这些元素布局。这些字段仍然由 Stripe 控制,以降低您的 PCI 合规成本。您可以在此处查看有关此内容的文档。
正如 koopajah 所说,这不是你可以用 PaymentElement 完成的事情。
为了实现你想要的,我们必须单独渲染每个元素。建议的方法可以使用
NextJs
和 TailwindCSS
来实现,如下所示:
StripeFormWrapper.tsx(这是我们自定义的条纹形式)
"use client"
import React, { useEffect, useState } from "react"
import { CardNumberElement, Elements, useElements, useStripe } from '@stripe/react-stripe-js'
import {
loadStripe,
StripeCardCvcElementOptions,
StripeCardExpiryElementOptions,
StripeCardNumberElement,
StripeCardNumberElementOptions
} from '@stripe/stripe-js'
import ButtonPrimary from "@/shared/ButtonPrimary"
import Checkbox from "@/shared/Checkbox"
import InputField from "@/shared/InputField"
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string)
function onVisible(element: HTMLElement, callback: Function): void {
new IntersectionObserver((entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
entries.forEach(entry => {
if(entry.intersectionRatio > 0) {
callback(element)
observer.disconnect()
}
})
}).observe(element)
}
const StripeForm = () => {
const stripe = useStripe()
const elements = useElements()
const [cardholderName, setCardholderName] = useState<string>("")
const [cardNumberReady, setCardNumberReady] = useState<boolean>(false)
const [cardExpiryReady, setCardExpiryReady] = useState<boolean>(false)
const [cardCvcReady, setCardCvcReady] = useState<boolean>(false)
useEffect(() => {
let cardNumber
let cardExpiry
let cardCvc
if (elements) {
cardNumber = elements.create(
"cardNumber",
{
classes: {
base: "table-cell align-middle w-[600px] h-[50px] pl-4 pr-2 py-2 rounded-full border border-solid border-zinc-400 bg-zinc-100"
},
showIcon: true,
} as StripeCardNumberElementOptions
)
cardNumber.mount("#cardNumber-element")
cardNumber.on("ready", () => setCardNumberReady(true))
cardExpiry = elements.create(
"cardExpiry",
{
classes: {
base: "table-cell align-middle w-[600px] h-[50px] pl-4 pr-2 py-2 rounded-full border border-solid border-zinc-400 bg-zinc-100"
},
} as StripeCardExpiryElementOptions
)
cardExpiry.mount("#cardExpiry-element")
cardExpiry.on("ready", () => setCardExpiryReady(true))
cardCvc = elements.create(
"cardCvc",
{
classes: {
base: "table-cell align-middle w-[600px] h-[50px] pl-4 pr-2 py-2 rounded-full border border-solid border-zinc-400 bg-zinc-100"
},
} as StripeCardCvcElementOptions
)
cardCvc.mount("#cardCvc-element")
cardCvc.on("ready", () => setCardCvcReady(true))
}
}, [elements])
const handleSubmit = async (e: React.FormEvent) => { // tweak the content according to your needs
e.preventDefault()
const cardNumberElement = elements!.getElement(CardNumberElement)
const response = await stripe!.confirmCardSetup(
"client_secret", // from intent
{
payment_method: {
card: cardNumberElement!,
billing_details: {
name: cardholderName,
address: {
city: "Denver",
country: "US",
line1: "US, Denver, Colorado",
line2: "",
state: "Colorado",
postal_code: "80221"
},
email: "[email protected]", // customer email in stripe
}
},
}
)
console.log("response", response)
}
// for presentation purposes
const cardNumberRef = React.useRef<HTMLDivElement>(null)
const [showCardNumber, setShowCardNumber] = useState<boolean>(false)
useEffect(() => {
if (cardNumberRef.current) {
onVisible(cardNumberRef.current, () => {
setShowCardNumber(true)
})
}
}, [cardNumberRef.current])
const cardExpiryRef = React.useRef<HTMLDivElement>(null)
const [showCardExpiry, setShowCardExpiry] = useState<boolean>(false)
useEffect(() => {
if (cardExpiryRef.current) {
onVisible(cardExpiryRef.current, () => {
setShowCardExpiry(true)
})
}
}, [cardExpiryRef.current])
const cardCvcRef = React.useRef<HTMLDivElement>(null)
const [showCardCvc, setShowCardCvc] = useState<boolean>(false)
useEffect(() => {
if (cardCvcRef.current) {
onVisible(cardCvcRef.current, () => {
setShowCardCvc(true)
})
}
}, [cardCvcRef.current])
return (
<React.Fragment>
<form
className="w-full grid grid-cols-1 gap-6"
onSubmit={handleSubmit}
>
<label className="block">
<InputField
name="name"
label={(() => (
<div className="mb-2">
<h2 className="text-sm font-medium leading-normal">
<span className="text-zinc-700">Name of Cardholder</span>
</h2>
</div>
))()}
borderClassName="rounded-3xl"
type="text"
placeholder="Cardholder Name"
value={cardholderName}
onChange={(event) => setCardholderName(event.target.value)}
/>
</label>
<label className="inline-block">
{showCardNumber && (
<div className={`mb-2 ${cardNumberReady ? `opacity-100` : `opacity-25`}`}>
<h2 className="text-sm font-medium leading-normal">
<span className="text-zinc-700">Card Number</span>
</h2>
</div>
)}
<div
id="cardNumber-element"
style={{ opacity: `${cardNumberReady ? `1` : `0.25`}` }}
ref={cardNumberRef}
/>
</label>
<label className="inline-block">
<div className="flex space-x-3">
<div className="basis-1/2">
{showCardExpiry && (
<div className={`mb-2 ${cardExpiryReady ? `opacity-100` : `opacity-25`}`}>
<h2 className="text-sm font-medium leading-normal">
<span className="text-zinc-700">Expiry Date</span>
</h2>
</div>
)}
<div
id="cardExpiry-element"
style={{ opacity: `${cardExpiryReady ? `1` : `0.25`}` }}
ref={cardExpiryRef}
/>
</div>
<div className="basis-1/2">
{showCardCvc && (
<div className={`mb-2 ${cardCvcReady ? `opacity-100` : `opacity-25`}`}>
<h2 className="text-sm font-medium leading-normal">
<span className="text-zinc-700">Security Code (CVV)</span>
</h2>
</div>
)}
<div
id="cardCvc-element"
style={{ opacity: `${cardCvcReady ? `1` : `0.25`}` }}
ref={cardCvcRef}
/>
</div>
</div>
</label>
<label className="inline-block">
<Checkbox
name="agreed"
className="!text-xs h-6 items-center"
inputClassName="cursor-pointer focus:ring-0 dark:focus:ring-0 h-[20px] w-[20px] rounded bg-zinc-50 dark:bg-zinc-700 checked:bg-[url('../images/checked.svg')] checked:bg-contain dark:checked:bg-current checked:bg-[length:9px_7px] border-zinc-400 dark:border-zinc-200 text-zinc-700"
labelClassName="!ml-1"
label={(() => {
return (
<h2 className="text-sm font-normal leading-[20px]">
<span className="text-zinc-700">Save Card for Future Transactions</span>
</h2>
)
})()}
defaultChecked={saveCard}
onChange={(checked) => setSaveCard(checked)}
/>
</label>
<label className="inline-block">
<ButtonPrimary
containerClassName="w-full"
className="w-full mx-auto !py-1.5 !pr-1.5 !pl-[14px] justify-between"
type="submit"
showIcon
>
Save
</ButtonPrimary>
</label>
</form>
</React.Fragment>
)
}
const StripeFormWrapper = () => (
<Elements
stripe={stripePromise}
>
<StripeForm />
</Elements>
)
export default StripeFormWrapper
page.tsx(这是页面)
import React, { FC } from "react"
import StripeFormWrapper from "./StripeFormWrapper"
export interface AddPaymentMethodPageProps {}
const AddPaymentMethodPage: FC<AddPaymentMethodPageProps> = ({}) => {
return (
<div className={`nc-AddPaymentMethodPage overflow-hidden`}>
<div className="container relative mb-24 lg:mb-28">
<div className="pt-5 lg:pt-8 pb-8">
<div className="flex flex-col items-center max-w-[600px] mx-auto space-y-6">
<h2 className="w-full text-4xl sm:text-5xl text-center font-normal leading-normal tracking-[-0.48px]">
<span className="text-zinc-500">Add </span>
<span className="text-zinc-700">Payment Method</span>
</h2>
<div className="flex flex-col items-start self-stretch">
<StripeFormWrapper />
</div>
</div>
</div>
</div>
</div>
)
}
结果