คู่มือการฝังหน้าชำระเงินผ่าน iframe (สำหรับร้านค้า/Merchant)
เอกสารนี้อธิบายวิธีฝัง "หน้าชำระเงิน" (Payment Page) ลงในเว็บไซต์ของร้านค้าผ่าน <iframe>
ให้ทำงานได้สมบูรณ์ โดยเฉพาะปุ่ม "คัดลอก" (Copy) เลขบัญชี / จำนวนเงิน / รหัสอ้างอิง
หน้าชำระเงินคือ URL (
payment_url) ที่ระบบคืนกลับมาจาก endpoint FLEX (/payment-flex/create) — ดูหัวข้อ "สร้างคำสั่งชำระเงิน — FLEX (แนะนำ)"
สรุปสาเหตุที่ลูกค้ากด "คัดลอก" ไม่ได้: หน้าร้านค้าที่ฝัง iframe เป็น HTTP ทำให้ เบราว์เซอร์ถือว่า iframe ทั้งอันอยู่ใน "บริบทที่ไม่ปลอดภัย" (non-secure context) และ ปิดการใช้งาน Clipboard API ทั้งหมด แก้ได้ตามข้อ 1–2 ด้านล่าง
✅ เช็กลิสต์สำคัญ (ทำตามนี้ครบ จบ)
- [ ] หน้าเว็บที่ฝัง iframe ต้องเป็น HTTPS (ห้ามเป็น HTTP เด็ดขาด)
- [ ] ใส่แอตทริบิวต์
allow="clipboard-write"ที่แท็ก<iframe> - [ ] ตั้งความกว้าง/สูงให้พอ (กว้าง ≥ ~360px) หรือเต็มจอบนมือถือ — ถ้ากรอบกว้างกว่า 480px หน้าจะ ซูมเนื้อหาให้พอดีอัตโนมัติ (ดูข้อ 3)
- [ ] อย่า ใส่
sandboxถ้าไม่จำเป็น (ถ้าจำเป็นจริง ดูข้อ 4)
โค้ดที่ถูกต้อง (คัดลอกไปใช้ได้เลย)
<iframe
src="{{payment_url}}"
allow="clipboard-write"
title="ชำระเงิน"
style="width:100%; max-width:480px; height:860px; border:0; border-radius:12px;">
</iframe>
กำหนด
srcด้วยค่าpayment_urlที่ได้จาก response ของ API ตอนสร้างออเดอร์ — ใช้ค่าที่ได้มาตรง ๆ อย่า hardcode โดเมนหรือประกอบ URL เอง ให้ถือว่าpayment_urlเป็นค่าทึบ (opaque) เพราะรูปแบบอาจเปลี่ยนได้ในอนาคต
1. ทำไมหน้าเว็บที่ฝัง iframe ต้องเป็น HTTPS (ข้อสำคัญที่สุด)
แม้ตัวหน้าชำระเงินจะเป็น HTTPS อยู่แล้ว แต่ตามมาตรฐานความปลอดภัยของเบราว์เซอร์ (Secure Contexts) iframe จะถือว่า "ปลอดภัย" ก็ต่อเมื่อ ทั้งตัวมันเองและหน้าแม่ (หน้าที่ฝังมัน) เป็น HTTPS ทุกชั้น
ดังนั้นถ้าหน้าร้านค้าเป็น HTTP → iframe ทั้งอันกลายเป็น non-secure context → เบราว์เซอร์จะ:
- ปิด
navigator.clipboard(Clipboard API) → ปุ่ม "คัดลอก" ใช้ไม่ได้ ← อาการที่ลูกค้าเจอ - ปิดฟีเจอร์อื่นที่ต้องการบริบทปลอดภัยด้วย (กล้อง, ตำแหน่ง, ฯลฯ)
วิธีแก้: ให้บริการหน้าร้านค้าที่ฝาก iframe ผ่าน HTTPS เท่านั้น
หมายเหตุ: ในทางกลับกัน หน้าแม่ที่เป็น HTTPS จะ ฝาก iframe ที่เป็น HTTP ไม่ได้ (เบราว์เซอร์บล็อกเป็น mixed content) — แต่ของเราเป็น HTTPS อยู่แล้วจึงไม่มีปัญหานี้
2. ทำไมต้องใส่ allow="clipboard-write"
นอกจากเรื่อง HTTPS แล้ว การเขียนข้อมูลลงคลิปบอร์ดจาก iframe ที่มาจาก คนละโดเมน
(cross-origin) ยังถูกควบคุมด้วย Permissions Policy อีกชั้น โดยค่าเริ่มต้นเบราว์เซอร์
จะไม่อนุญาตให้ iframe ข้ามโดเมนเขียนคลิปบอร์ด เว้นแต่หน้าแม่ "มอบสิทธิ์" ให้ผ่าน
แอตทริบิวต์ allow
<iframe src="{{payment_url}}" allow="clipboard-write"></iframe>
- ต้องการแค่
clipboard-write(หน้าชำระเงินเขียนคลิปบอร์ดอย่างเดียว ไม่ได้อ่าน) - ไม่ต้องใส่
clipboard-readหรือสิทธิ์อื่น
ระบบของเราได้เพิ่มกลไกสำรอง (fallback) ฝั่งหน้าชำระเงินไว้แล้ว เพื่อให้คัดลอกได้ แม้ในกรณีที่ Clipboard API ถูกปิด แต่การทำข้อ 1–2 ให้ถูกต้องคือวิธีที่ดีและเสถียรที่สุด
3. ขนาดและการแสดงผล (Responsive + ปรับขนาดอัตโนมัติ)
หน้าชำระเงินออกแบบมาแบบ mobile-first (ดีไซน์อ้างอิงความกว้าง ~480px) แนะนำ:
- มือถือ: ให้เต็มความกว้างจอ และสูงเต็มจอเพื่อประสบการณ์ที่ดีที่สุด
- เดสก์ท็อป / กล่องแคบ: กว้างประมาณ 420–480px, สูงประมาณ 820–880px
- ความกว้างไม่ควรน้อยกว่า ~360px (มิฉะนั้นเนื้อหาจะอัดแน่นเกินไป)
ปรับเนื้อหาให้พอดีกรอบอัตโนมัติ (auto-fit / zoom)
แต่เดิม ถ้าตั้ง <iframe> ให้ กว้างกว่า ~480px เนื้อหาจะถูกจัดกึ่งกลางแล้วเหลือขอบว่าง
ซ้าย-ขวา และตัวหนังสือดูเล็กอ่านยาก ตอนนี้หน้าชำระเงิน ตรวจจับว่าตัวเองถูกฝังอยู่ใน iframe
แล้วปรับ (zoom) เนื้อหาให้พอดีความกว้างกรอบโดยอัตโนมัติ — ไม่ต้องตั้งค่าอะไรเพิ่มจากฝั่งร้านค้า
ความกว้างของ <iframe> | พฤติกรรมของหน้าชำระเงิน |
|---|---|
| ≤ 480px | แสดงแบบมือถือปกติ เต็มความกว้าง (ไม่ซูม) |
| 480–620px | ซูมเนื้อหาขึ้นให้เต็มกรอบ — ตัวหนังสือใหญ่ขึ้น อ่านง่าย ไม่มีขอบว่าง |
| 620–1024px | คงระดับซูมไว้เท่าที่ 620px เนื้อหาอยู่กึ่งกลาง มีขอบว่างซ้าย-ขวาเล็กน้อย |
| ≥ 1024px | สลับเป็น เลย์เอาต์เดสก์ท็อป 2 คอลัมน์ อัตโนมัติ |
⚠️ เรื่องความสูง: เมื่อเนื้อหาถูกซูมขึ้น (กรอบกว้าง 480–620px) เนื้อหาจะ สูงขึ้นตาม สัดส่วนด้วย ดังนั้นถ้าตั้งกรอบให้กว้างขึ้น ควรเพิ่มค่า
heightตามไปด้วย (ประมาณความสูงที่ 480px × ความกว้างกรอบ ÷ 480) มิฉะนั้นจะเกิดแถบเลื่อน (scrollbar) ภายในกรอบ ตัวอย่าง: กว้าง 480px ใช้สูง ~860px → กว้าง 600px ควรใช้สูง ~1075px
💡 ตั้งค่าง่ายที่สุด: คงความกว้างไว้ที่ ≤ 480px (ไม่ต้องคำนวณความสูงเพิ่ม) แต่ถ้ามีพื้นที่ กว้างกว่านั้นก็ฝังได้เลย เนื้อหาจะปรับให้อ่านง่ายอัตโนมัติ และถ้ากว้างถึงระดับเดสก์ท็อป (≥1024px) จะได้เลย์เอาต์ 2 คอลัมน์เต็มรูปแบบ
<style>
.pay-frame {
width: 100%;
max-width: 480px;
height: 860px;
border: 0;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,.12);
}
@media (max-width: 520px) {
.pay-frame { max-width: 100%; height: 100vh; border-radius: 0; box-shadow: none; }
}
</style>
<iframe class="pay-frame"
src="{{payment_url}}"
allow="clipboard-write"
title="ชำระเงิน"></iframe>
หน้าชำระเงินจะปรับ ความกว้างของเนื้อหา ให้พอดีกรอบโดยอัตโนมัติ (ดูหัวข้อย่อยด้านบน) แต่ ไม่ได้ ส่งสัญญาณปรับ ความสูง ของ iframe (auto-resize) กลับไปยังหน้าแม่ จึงควร กำหนด
heightให้พอกับเนื้อหา (เผื่อกรณีถูกซูมตามตาราง) หรือให้เต็มจอบนมือถือ
4. ถ้าจำเป็นต้องใช้ sandbox
โดยปกติ ไม่แนะนำให้ใส่ sandbox เพราะจะปิดความสามารถหลายอย่างจนหน้าชำระเงิน
ทำงานไม่ครบ แต่ถ้าจำเป็นต้องใส่ ให้ใส่ token เหล่านี้ครบ:
<iframe
src="{{payment_url}}"
allow="clipboard-write"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads allow-top-navigation-by-user-activation"
title="ชำระเงิน"></iframe>
| token | จำเป็นเพราะ |
|---|---|
allow-scripts | หน้าชำระเงินเป็น JavaScript ทั้งหมด |
allow-same-origin | ใช้สำหรับ WebSocket (อัปเดตสถานะเรียลไทม์), คลิปบอร์ด, การจำค่า |
allow-forms | การอัปโหลดสลิป (slip) |
allow-popups | กรณีเปิดหน้าต่าง/ลิงก์เพิ่มเติม |
allow-downloads | การบันทึก/ดาวน์โหลด QR หรือสลิป |
allow-top-navigation-by-user-activation | ปุ่ม "กลับสู่หน้าร้านค้า" ให้พาออกจาก iframe ได้ (ดูข้อ 5) |
⚠️ ถ้าใส่
sandboxแต่ขาดallow-same-originหรือallow-scriptsหน้าชำระเงินจะ ทำงานไม่ได้เลย
5. ปุ่ม "กลับสู่หน้าร้านค้า" (Return flow)
เมื่อชำระเสร็จ/หมดเวลา หน้าชำระเงินจะแสดงปุ่มกลับไปยัง redirect_url ที่ร้านค้ากำหนด
ตอนสร้างรายการ โดยปุ่มนี้จะเปลี่ยนหน้าภายในกรอบ iframe (ไม่ได้พาออกทั้งหน้า)
ผลคือ ถ้า redirect_url ชี้ไปหน้าปกติของร้านค้า มันจะไปโหลด อยู่ในกรอบ iframe เล็ก ๆ
ซึ่งไม่ใช่สิ่งที่ต้องการ แนะนำ 1 ใน 2 วิธี:
วิธี A (แนะนำ): ตั้ง redirect_url ให้ชี้ไปหน้า "เด้งออกจากกรอบ" เล็ก ๆ บนเว็บร้านค้า เช่น
<!-- เช่น https://shop.example.com/return.html — ใช้เป็น redirect_url -->
<!doctype html>
<meta charset="utf-8">
<script>
// พาทั้งหน้า (top window) ออกจาก iframe ไปยังปลายทางจริง
var dest = "https://shop.example.com/orders/success?id=12345";
(window.top || window).location.replace(dest);
</script>
วิธี B: ยอมรับว่าหน้าปลายทางจะแสดงในกรอบ iframe และทำให้หน้านั้น ฝังใน iframe ได้
(ต้องไม่ตั้ง X-Frame-Options: DENY หรือ CSP frame-ancestors ที่บล็อก)
ถ้าใช้
sandboxต้องมีallow-top-navigation-by-user-activationเพื่อให้วิธี A ทำงาน
6. ทางเลือกที่เสถียรที่สุด (พิจารณาเพิ่มเติม)
การฝากหน้าชำระเงินใน iframe ใช้งานได้ดีถ้าตั้งค่าตามข้างต้น แต่ถ้าต้องการความเสถียร สูงสุดและลดปัญหาเฉพาะเบราว์เซอร์ (เช่น การจำกัดคุกกี้ของบุคคลที่สามใน Safari, การเด้งหน้า 3DS ของธนาคาร, ป๊อปอัป) อาจพิจารณา:
- เปลี่ยนหน้าเต็ม (full-page redirect): พาผู้ใช้ไปหน้าชำระเงินทั้งหน้า แล้วค่อย
redirect_urlกลับมาที่ร้านค้า — เสถียรที่สุด - เปิดแท็บ/หน้าต่างใหม่: เปิดหน้าชำระเงินในแท็บใหม่
ทั้งสองวิธีไม่มีข้อจำกัดเรื่อง secure context / clipboard / cookie ของ iframe เลย
7. การทดสอบ (Checklist ก่อนขึ้นจริง)
- [ ] เปิดหน้าร้านค้าที่ฝัง iframe ด้วย HTTPS จริง (ไม่ใช่
localhost) - [ ] กดปุ่ม "คัดลอก" จำนวนเงิน / เลขบัญชี / รหัสอ้างอิง → ต้องขึ้น "คัดลอกแล้ว" (สีเขียว)
และวางค่าได้จริง - [ ] ทดสอบบนมือถือจริง (Safari iOS และ Chrome Android) — เป็น in-app browser ด้วยถ้าทำได้
(เช่นเปิดผ่าน LINE/Facebook) - [ ] ทดสอบฝังที่ความกว้างหลายค่า (เช่น 480px, 600px, เต็มจอ) → เนื้อหาพอดีกรอบ
ตัวหนังสือไม่เล็กผิดปกติ และไม่มีขอบว่างซ้าย-ขวาเกินจำเป็น (ดูข้อ 3) - [ ] ทดสอบปุ่ม "กลับสู่หน้าร้านค้า" ว่าพากลับถูกที่ (ดูข้อ 5)
- [ ] ทดสอบอัปโหลดสลิป (ถ้าระบบใช้)
ติดต่อ
หากตั้งค่าตามคู่มือแล้วยังพบปัญหา กรุณาแจ้งทีมงานพร้อม: ชนิดเบราว์เซอร์/อุปกรณ์,
URL หน้าร้านค้าที่ฝัง (เป็น HTTP หรือ HTTPS), และโค้ดแท็ก <iframe> ที่ใช้