@inverz/card-input
v1.1.0
Published
Credit card input for React with a flipping card preview: brand detection, Luhn check, expiry and CVC validation, Thai and English messages
Readme
@inverz/card-input
ช่องกรอก บัตรเครดิต / เดบิต สำหรับ React พร้อม บัตรจำลองที่พลิกไปด้านหลัง ตอนกรอก CVV รู้ประเภทบัตรทันที จัดรูปแบบตามประเภท ตรวจเลขบัตร (Luhn) วันหมดอายุ และ CVV ข้อความภาษาไทย/อังกฤษ แล้วส่งต่อให้ payment gateway แปลงเป็น token
A React credit card input with a flipping card preview: brand detection, Luhn check, expiry and CVC validation, Thai and English messages, 7 flip animations.
แพ็กเกจนี้ตรวจรูปแบบเท่านั้น ส่งข้อมูลบัตรให้ gateway (Omise, 2C2P, Stripe ฯลฯ) แปลงเป็น token จากเบราว์เซอร์ แล้วส่งแค่ token ไปที่ server ห้ามส่งหรือเก็บเลขบัตรใน server ของเราเอง (ข้อกำหนด PCI DSS)
ติดตั้ง
npm i @inverz/card-inputimport "@inverz/card-input/style.css";
import { CreditCardInput } from "@inverz/card-input";
const [card, setCard] = useState({});
<CreditCardInput value={card} onChange={(value, info) => setCard(value)} />ค่าที่ได้:
value = { number: "4242424242424242", name: "SOMCHAI JAIDEE", expiry: "12/28", cvc: "123" }
info = { brand: "visa", valid: true, errors: {}, expMonth: 12, expYear: 2028 }สิ่งที่ทำให้
- รู้ประเภทบัตร: Visa, Mastercard (รวมเลข 2-series), American Express, JCB, UnionPay, Discover, Diners Club
- จัดช่องว่างตามประเภท (
4242 4242 4242 4242, Amex3782 822463 10005) และตัดความยาวตามประเภท - ตรวจเลขด้วย Luhn ถ้าผิดจะบอกแค่ว่า "เลขบัตรไม่ถูกต้อง" ไม่บอกเลขที่ถูก
- วันหมดอายุพิมพ์ตัวเลขแล้วใส่
/ให้ ตรวจเดือน 01–12 และบัตรหมดอายุ (ใช้ได้ถึงสิ้นเดือนที่ระบุ) - CVV 3 หลัก หรือ 4 หลักสำหรับ Amex ซึ่งอยู่ด้านหน้าบัตร Amex จึงไม่พลิก
- บัตรจำลองแสดงเลข 4 ตัวแรกและ 4 ตัวท้าย ตัวกลางเป็น
*ทันทีที่พิมพ์ - ชื่อบนบัตรเป็นตัวพิมพ์ใหญ่ รับเฉพาะ A–Z และ
. ' - - ใส่
autocomplete(cc-number,cc-name,cc-exp,cc-csc) ให้เบราว์เซอร์กรอกบัตรที่บันทึกไว้ได้ - ข้อความผิดพลาดแสดงหลังออกจากช่อง ยกเว้นเลขบัตรผิด เดือนผิด และบัตรหมดอายุ ซึ่งแสดงทันที
Props
| prop | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|
| value / defaultValue | | { number, name, expiry, cvc } |
| onChange | | (value, info) => void |
| flipAnimation | "spin" | "spin" "flip" "lift" "vertical" "slide" "fade" "none" |
| respectReducedMotion | true | ถ้าเครื่องตั้ง "ลดการเคลื่อนไหว" จะเปลี่ยนด้านทันที ตั้ง false เพื่อเล่นแอนิเมชันเสมอ |
| showCard | true | แสดงบัตรจำลอง |
| showName | true | แสดงช่องชื่อบนบัตร |
| requireName | เท่ากับ showName | ต้องมีชื่อจึงจะ valid |
| locale | "th" | "en" ป้ายและข้อความภาษาอังกฤษ |
| disabled | false | |
CardPreview
ใช้ภาพบัตรอย่างเดียว กับช่องกรอกของคุณเอง หรือแสดงบัตรที่บันทึกไว้
<CardPreview number="5555555555554444" name="SOMYING JAIDEE" expiry="03/29" flipped={false} animation="spin" />ขนาดปรับด้วย --tcc-card-width (ค่าเริ่มต้น 340px) ตัวอักษรบนบัตรปรับตามขนาดให้เอง
ส่งให้ payment gateway
ข้อมูลบัตรต้องส่งให้ gateway แปลงเป็น token จากเบราว์เซอร์ แล้วส่งแค่ token ไปที่ server gateway แบ่งเป็น 2 แบบ:
| gateway | ใช้ CreditCardInput | วิธีรับบัตร |
|---|---|---|
| Omise / Opn Payments | ได้ | Omise.js สร้าง token จากช่องของเรา |
| 2C2P | ได้ | Secure Pay JS เข้ารหัสในเบราว์เซอร์ |
| Stripe | ใช้ StripeCardInput แทน (ดูด้านล่าง) | ช่องกรอกของ Stripe หน้าตาแบบ CreditCardInput |
| Adyen | ไม่ได้ | Drop-in / Card Component (iframe) |
| Braintree / PayPal | ไม่ได้ | Hosted Fields (iframe) |
| Checkout.com | ไม่ได้ | Frames (iframe) |
gateway ที่บังคับใช้ iframe ไม่ให้ส่งเลขบัตรจากช่องกรอกของเราเอง (ถ้าส่งต้องผ่าน PCI DSS ระดับสูงสุด) จึงใช้ CreditCardInput กับ gateway เหล่านั้นไม่ได้ ตัวอย่างเต็มทั้ง JavaScript และ TypeScript ของ Omise, 2C2P และ Stripe อยู่ใน demo
Omise / Opn Payments
// <script src="https://cdn.omise.co/omise.js"></script>
Omise.setPublicKey("pkey_test_…");
const [month, year] = card.expiry.split("/");
Omise.createToken("card", {
number: card.number,
name: card.name,
expiration_month: Number(month),
expiration_year: 2000 + Number(year),
security_code: card.cvc,
}, (status, res) => {
if (status === 200) fetch("/api/charge", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: res.id }) });
});
// server: npm i omise
const omise = require("omise")({ secretKey: process.env.OMISE_SECRET_KEY });
const charge = await omise.charges.create({ amount: 150000, currency: "thb", card: token, return_uri: "https://your-site.com/paid" });
// พาผู้ใช้ไปที่ charge.authorize_uri เพื่อยืนยัน 3-D Secure2C2P
Secure Pay JS อ่านค่าจากช่องที่มี data-encrypt ส่งต่อค่าจาก CreditCardInput ด้วย hidden input ที่ ไม่มี name แล้วส่งค่าที่เข้ารหัสไปใช้เป็น securePayToken ใน Do Payment API
// <script src="https://demo2.2c2p.com/SecurePayment/api/pgw-securepay-sdk-2.0.0.min.js"></script> (sandbox)
const [month, yy] = (card.expiry ?? "").split("/");
<form ref={formRef} onSubmit={(e) => { e.preventDefault(); pay(); }}>
<CreditCardInput value={card} onChange={setCard} />
<input type="hidden" data-encrypt="cardnumber" value={card.number ?? ""} />
<input type="hidden" data-encrypt="month" value={month ?? ""} />
<input type="hidden" data-encrypt="year" value={yy ? "20" + yy : ""} />
<input type="hidden" data-encrypt="cvv" value={card.cvc ?? ""} />
<button type="submit">ชำระเงิน</button>
</form>
const pay = () =>
My2c2p.getEncrypted(My2c2p.APIEnvironment.Sandbox, formRef.current, (formData, errorCode, errorDesc) => {
if (errorCode) return alert(errorDesc);
fetch("/api/2c2p/pay", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ securePayToken: formData.encryptedCardInfo }) });
});
// server: ขอ paymentToken (Payment Token API, เข้ารหัส JWT ด้วย secret key) แล้วส่ง Do Payment
// POST https://sandbox-pgw.2c2p.com/payment/4.3/payment
// { paymentToken, payment: { code: { channelCode: "CC" }, data: { securePayToken } } }
// respCode "1001" = พาผู้ใช้ไปที่ data (หน้า 3-D Secure)Stripe
Stripe ไม่ให้ส่งเลขบัตรจากช่องกรอกของเราเอง มี 2 ทาง:
ทางที่ 1 (แนะนำ): StripeCardInput จาก @inverz/card-input/stripe ใช้ช่องกรอกของ Stripe (CardNumberElement, CardExpiryElement, CardCvcElement) เลขบัตรอยู่ใน iframe ของ Stripe เว็บเราไม่เห็นเลขบัตร (PCI แบบ SAQ A) แต่ได้หน้าตา บัตรพลิก สีตามประเภทบัตร และข้อความภาษาไทยเหมือน CreditCardInput ข้อต่างคือบัตรจำลองโชว์เลขจริงไม่ได้ เพราะ Stripe ไม่ส่งตัวเลขออกมา
npm i @inverz/card-input @stripe/stripe-js @stripe/react-stripe-jsimport { loadStripe } from "@stripe/stripe-js";
import { Elements, CardNumberElement, useStripe, useElements } from "@stripe/react-stripe-js";
import { StripeCardInput } from "@inverz/card-input/stripe";
import "@inverz/card-input/style.css";
const stripePromise = loadStripe("pk_test_…");
<Elements stripe={stripePromise} options={{ locale: "th" }}>
<Checkout clientSecret={clientSecret} />
</Elements>
const Checkout = ({ clientSecret }) => {
const stripe = useStripe();
const elements = useElements();
const [info, setInfo] = useState({ valid: false });
const [name, setName] = useState("");
const pay = async () => {
const { error } = await stripe.confirmCardPayment(clientSecret, {
payment_method: { card: elements.getElement(CardNumberElement), billing_details: { name } },
});
if (error) alert(error.message);
};
return (
<>
<StripeCardInput name={name} onNameChange={setName} onChange={setInfo} />
<button disabled={!stripe || !info.valid} onClick={pay}>ชำระเงิน</button>
</>
);
};
// server: stripe.paymentIntents.create({ amount: 150000, currency: "thb", payment_method_types: ["card"] }) → client_secret| prop | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|
| onChange | | (info) => void ได้ { brand, valid, errors, name } |
| name / defaultName / onNameChange | | ชื่อบนบัตร (ช่องนี้เป็นของเรา ไม่ใช่ข้อมูลบัตร) |
| showCard, showName, requireName | true | |
| flipAnimation, respectReducedMotion, locale, disabled | | เหมือน CreditCardInput |
| elementStyle | | style ที่ส่งเข้า iframe ของ Stripe (CSS variables เข้าไปใน iframe ไม่ได้) |
ทางที่ 2: ส่งจาก CreditCardInput ตรงๆ ใช้ได้เฉพาะเมื่อ Stripe เปิดสิทธิ์ส่งข้อมูลบัตรเอง (raw card data APIs) ให้บัญชีของคุณแล้ว ซึ่งต้องยื่น PCI DSS แบบ SAQ D ทุกปี ถ้ายังไม่ได้เปิด Stripe จะปฏิเสธคำขอ
import { toStripeCardParams } from "@inverz/card-input";
const r = await fetch("https://api.stripe.com/v1/payment_methods", {
method: "POST",
headers: { Authorization: "Bearer pk_live_…", "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(toStripeCardParams(card)), // { type: "card", "card[number]": …, "card[exp_month]": …, … }
});
const pm = await r.json(); // ส่ง pm.id ไป server เพื่อ confirm PaymentIntentทุกตัวอย่างใช้ key ทดสอบ ลองกับ sandbox ของ gateway ก่อนใช้จริง และตรวจกับเอกสารของ gateway รุ่นที่ใช้: Omise.js · 2C2P card encryption · 2C2P Do Payment · Stripe Payment Element · Stripe: create a PaymentMethod
ฟังก์ชัน
@inverz/card-input/core ไม่ดึง React
import { detectCardBrand, formatCardNumber, isValidCardNumber, validateCard, TEST_CARDS } from "@inverz/card-input/core";
detectCardBrand("4242")?.name; // "Visa"
formatCardNumber("378282246310005"); // "3782 822463 10005"
isValidCardNumber("4242424242424241"); // false
validateCard({ number, expiry: "12/28", cvc: "123" }, { requireName: false });
// { valid: true, brand: "visa", errors: {} }| ฟังก์ชัน | คำอธิบาย |
|---|---|
| CARD_BRANDS | ข้อมูลแต่ละประเภท (pattern, ความยาว, ตำแหน่งช่องว่าง, ความยาว CVC) |
| detectCardBrand(number) | ประเภทบัตรจากเลขบางส่วน หรือ null |
| luhn(number) | ตรวจ Luhn |
| isValidCardNumber, isCompleteCardNumber | ความยาวตามประเภท + Luhn |
| formatCardNumber, maskCardNumber | จัดรูปแบบ / ซ่อนเลขกลาง 4242 **** **** 4242 |
| formatExpiry, parseExpiry, expiryError, isValidExpiry | วันหมดอายุ |
| cvcLength, isValidCvc | CVC |
| validateCard(card, options) | ตรวจทั้งใบ ได้ errors เป็นรหัส ("invalid", "expired" …) |
| toStripeCardParams(value) | ค่าจาก CreditCardInput เป็นพารามิเตอร์ของ Stripe POST /v1/payment_methods (ต้องได้สิทธิ์ raw card data จาก Stripe) |
| TEST_CARDS | เลขบัตรทดสอบที่เครือข่ายบัตรและ gateway เผยแพร่ ผ่านการตรวจรูปแบบแต่ตัดเงินจริงไม่ได้ (บาง gateway มีเลขทดสอบของตัวเอง) |
ธีม
ปรับด้วย CSS variables --tcc-brand, --tcc-surface, --tcc-border-strong, --tcc-text, --tcc-danger, --tcc-font, --tcc-card-width, --tcc-card-font
ชื่อและเครื่องหมายของบัตรเป็นของเจ้าของแต่ละราย บัตรจำลองใช้เพียงชื่อประเภทเป็นตัวอักษร ไม่มีโลโก้
License
MIT © 2026 Inverz Solutions Co.,Ltd.
