ระหว่างทดสอบ IAP ก่อนปล่อยขึ้นสโตร์ เราเจอปัญหาคลาสสิกที่ทำให้งงมาก:

  • ผู้ใช้กด “ดำเนินการซื้อต่อ”
  • ปุ่มเปลี่ยนเป็น “กำลังประมวลผล”
  • ผ่านไปไม่กี่วินาที ปุ่มกลับมาเป็น “ดำเนินการซื้อต่อ”
  • ไม่มีหน้าต่างยืนยันการซื้อจาก App Store เด้งขึ้นมา

บทความนี้สรุปเส้นทางการตรวจสอบทั้งหมด, ล็อกสำคัญ, ต้นตอของปัญหา และแนวทางแก้ที่ใช้ได้จริงก่อนขึ้นโปรดักชัน

1. อาการที่พบ

หน้ารายการสินค้าซื้อในแอปโหลดได้ปกติ แต่พอกดซื้อ:

  • มีการเรียก purchase()
  • ไม่มี system dialog
  • UI กลับไปสถานะยังไม่ซื้อในที่สุด

มองเผิน ๆ เหมือน “การซื้อไม่ได้เริ่มเลย” แต่ล็อกบอกอีกอย่าง

2. ตรวจรอบแรก: flow การซื้อเดินจริงหรือไม่

เราเพิ่มล็อกในจุดสำคัญทั้งหมดของเส้นทางการซื้อ:

  • พารามิเตอร์ตอนกดปุ่ม
  • จุดเข้า purchaseSelectedProduct() และสาขา guard
  • ก่อน/หลัง product.purchase()
  • Transaction.updates
  • refreshEntitlements()
  • Transaction.currentEntitlements
  • Transaction.latest(for:)

ผลที่ได้:

  • purchase() คืนค่า .success
  • ตรวจสอบธุรกรรมผ่าน และมีการเรียก transaction.finish()
  • แต่ entitledIDs ยังว่าง

สรุปคือ: ปัญหาไม่ใช่ “ไม่ได้เริ่มซื้อ” แต่เป็น “ซื้อแล้วระบบไม่รับรู้ entitlement”

3. สิ่งที่เจอหลังขุดลึก

เมื่อเพิ่มล็อกละเอียดขึ้น พบสัญญาณชี้ชัด:

  1. จำนวนรอบของ Transaction.currentEntitlements เป็น 0
  2. Transaction.latest(for: monthly) มีเรคอร์ดถูกต้อง แต่หมดอายุแล้ว
  3. จุดสำคัญ: ก่อนซื้อมีธุรกรรมเก่าอยู่ใน Transaction.unfinished
  4. purchase() รอบนี้คืนธุรกรรมจาก chain เก่า ไม่ใช่จาก flow ซื้อใหม่แบบโต้ตอบ

จึงอธิบายได้ว่าทำไม “ไม่เด้งหน้าต่าง”:

  • StoreKit ส่งธุรกรรมเก่าที่ยังประมวลผลได้กลับมาก่อน (unfinished / historical chain)
  • โค้ดเราไป finish() ธุรกรรมนั้น
  • คำขอดูเหมือน “สำเร็จ” แต่ไม่ได้เข้าทางยืนยันการซื้อใหม่ที่ผู้ใช้คาดหวัง

4. สรุปสาเหตุหลัก

นี่ไม่ใช่ปัญหาแบบจุดเดียว แต่เกิดจากสองปัจจัยซ้อนกัน:

  • unfinished transaction รบกวน flow การซื้อ
  • subscription chain ของ sandbox อยู่ในสถานะหมดอายุ (แม้มี latest transaction ก็อาจไม่ใช่ active entitlement)

ผลลัพธ์สุดท้ายคือ: ไม่ขึ้นป๊อปอัป, เหมือนสำเร็จ, แต่ entitlement ว่าง

5. แนวทางแก้ (พิสูจน์แล้วว่าใช้ได้จริง)

5.1 ล้าง unfinished transactions เชิงรุกก่อนซื้อ

ตอนแอปเริ่มทำงานและก่อนกดซื้อ ให้สแกน Transaction.unfinished แล้วเรียก finish() กับธุรกรรมที่ verified เพื่อกันธุรกรรมเก่ามาแทรก flow ใหม่

5.2 เก็บและเสริมล็อกวินิจฉัย (แนะนำให้เปิดก่อนปล่อยจริง)

ล็อกที่ควรเก็บไว้จนกว่าจะปล่อย:

  • snapshot ของ unfinished transaction
  • subscription status (state, renewal info)
  • จำนวนรอบของ currentEntitlements
  • รายละเอียด transaction/expiration จาก latest(for:)

5.3 ตรวจ entitlement แบบสองทาง (เสริมได้)

ถ้าบางสภาพแวดล้อม currentEntitlements ว่างชั่วคราว ให้ใช้ latest(for:) เป็น fallback ได้ (แต่ยังต้องเช็กว่า active จริง)

6. เช็กลิสต์ IAP ก่อนขึ้นสโตร์

  1. ก่อนทุกการทดสอบ ตรวจสถานะสมาชิกของบัญชี Sandbox (หมดอายุหรือไม่)
  2. พิมพ์จำนวน unfinished ในเส้นทางสำคัญ
  3. ทดสอบ 3 สถานการณ์: ซื้อครั้งแรก / ต่ออายุหลังหมดอายุ / กู้คืนการซื้อ
  4. ตรวจว่า currentEntitlements กับ latest สอดคล้องกัน
  5. ถ้าเจอ “ไม่เด้งหน้าต่าง” ให้เช็กก่อนว่าโดนเส้นทางธุรกรรมเก่าหรือไม่

7. บทสรุป

“กดซื้อแล้วไม่เด้งหน้าต่าง” ไม่ได้แปลว่าเป็นปัญหา UI หรือจังหวะเรียกอย่างเดียวเสมอไป
ใน StoreKit 2, สถานะธุรกรรม, คิว unfinished และวงจรชีวิต subscription ส่งผลโดยตรงต่อพฤติกรรมสุดท้าย

การเติมล็อกและขั้นตอนล้างธุรกรรมก่อนปล่อย จะช่วยลดปัญหา IAP ที่รีโปรดักซ์ยากในโปรดักชันได้มาก