คู่มือการฝังหน้าชำระเงินผ่าน 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 ด้านล่าง


✅ เช็กลิสต์สำคัญ (ทำตามนี้ครบ จบ)

  1. [ ] หน้าเว็บที่ฝัง iframe ต้องเป็น HTTPS (ห้ามเป็น HTTP เด็ดขาด)
  2. [ ] ใส่แอตทริบิวต์ allow="clipboard-write" ที่แท็ก <iframe>
  3. [ ] ตั้งความกว้าง/สูงให้พอ (กว้าง ≥ ~360px) หรือเต็มจอบนมือถือ — ถ้ากรอบกว้างกว่า 480px หน้าจะ ซูมเนื้อหาให้พอดีอัตโนมัติ (ดูข้อ 3)
  4. [ ] อย่า ใส่ 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 → เบราว์เซอร์จะ:

วิธีแก้: ให้บริการหน้าร้านค้าที่ฝาก 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>

ระบบของเราได้เพิ่มกลไกสำรอง (fallback) ฝั่งหน้าชำระเงินไว้แล้ว เพื่อให้คัดลอกได้ แม้ในกรณีที่ Clipboard API ถูกปิด แต่การทำข้อ 1–2 ให้ถูกต้องคือวิธีที่ดีและเสถียรที่สุด


3. ขนาดและการแสดงผล (Responsive + ปรับขนาดอัตโนมัติ)

หน้าชำระเงินออกแบบมาแบบ mobile-first (ดีไซน์อ้างอิงความกว้าง ~480px) แนะนำ:

ปรับเนื้อหาให้พอดีกรอบอัตโนมัติ (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 ของธนาคาร, ป๊อปอัป) อาจพิจารณา:

ทั้งสองวิธีไม่มีข้อจำกัดเรื่อง secure context / clipboard / cookie ของ iframe เลย


7. การทดสอบ (Checklist ก่อนขึ้นจริง)


ติดต่อ

หากตั้งค่าตามคู่มือแล้วยังพบปัญหา กรุณาแจ้งทีมงานพร้อม: ชนิดเบราว์เซอร์/อุปกรณ์, URL หน้าร้านค้าที่ฝัง (เป็น HTTP หรือ HTTPS), และโค้ดแท็ก <iframe> ที่ใช้