การเพิ่มการซื้อในแอปลงในแอป Flutter

1. บทนำ

อัปเดตล่าสุด 11-07-2023

การเพิ่มการซื้อในแอปลงในแอป Flutter ต้องมีการตั้งค่า App Store และ Play Store ให้ถูกต้อง รวมถึงต้องยืนยันการซื้อและให้สิทธิ์ที่จำเป็น เช่น สิทธิพิเศษในการสมัครใช้บริการ

ใน Codelab นี้ คุณจะต้องเพิ่มการซื้อในแอป 3 ประเภทลงในแอป (จัดไว้ให้) และยืนยันการซื้อเหล่านี้โดยใช้แบ็กเอนด์ของ DART กับ Firebase แอป Dash Clicker ที่จัดเตรียมไว้ให้มีเกมที่ใช้มาสคอต Dash เป็นสกุลเงิน คุณจะเพิ่มตัวเลือกการซื้อต่อไปนี้

  1. ตัวเลือกการซื้อซ้ำได้ 2,000 เส้นพร้อมกัน
  2. ซื้อการอัปเกรดครั้งเดียวเพื่อเปลี่ยน Dash สไตล์เก่าให้เป็น Dash สไตล์ทันสมัย
  3. การสมัครใช้บริการที่เพิ่มจำนวนคลิกที่สร้างขึ้นโดยอัตโนมัติเป็น 2 เท่า

ตัวเลือกการซื้อแรกให้ประโยชน์โดยตรงแก่ผู้ใช้เป็น 2,000 ขีดกลาง ซึ่งจะพร้อมใช้งานสำหรับผู้ใช้โดยตรงและสามารถซื้อได้หลายครั้ง วิธีนี้เรียกว่าสินค้าอุปโภคบริโภค เพราะเป็นการบริโภคโดยตรงและบริโภคได้หลายครั้ง

ตัวเลือกที่ 2 จะอัปเกรดแดชบอร์ดให้สวยงามยิ่งขึ้น ต้องซื้อเพียงครั้งเดียวและใช้ได้ตลอดไป การซื้อดังกล่าวเรียกว่า "สินค้าอุปโภคบริโภค" เนื่องจากแอปใช้การซื้อไม่ได้แต่จะใช้ได้ตลอดไป

ตัวเลือกการซื้อครั้งที่ 3 ซึ่งเป็นครั้งสุดท้ายคือการสมัครใช้บริการ ขณะที่การสมัครรับข้อมูลใช้งานอยู่ ผู้ใช้จะได้รับขีดกลางเร็วขึ้น แต่เมื่อหยุดชำระค่าสมัครใช้บริการ สิทธิประโยชน์ก็จะหายไปด้วย

บริการแบ็กเอนด์ (มีให้คุณด้วย) จะทำงานเป็นแอป Dart ยืนยันว่ามีการซื้อเกิดขึ้น และจัดเก็บโดยใช้ Firestore เราใช้ Firestore เพื่อทำให้กระบวนการง่ายขึ้น แต่คุณใช้บริการแบ็กเอนด์ประเภทใดก็ได้ในแอปเวอร์ชันที่ใช้งานจริง

300123416ebc8dc1.png 7145d0fffe6ea741.png 646317a79be08214.png

สิ่งที่คุณจะสร้าง

  • คุณจะขยายแอปเพื่อรองรับการซื้อและการสมัครใช้บริการแบบใช้แล้วหมดไป
  • นอกจากนี้ คุณยังขยายแอปแบ็กเอนด์ของ Dart เพื่อยืนยันและจัดเก็บรายการที่ซื้อได้ด้วย

สิ่งที่จะ ได้เรียนรู้

  • วิธีกำหนดค่า App Store และ Play Store ด้วยผลิตภัณฑ์ที่ซื้อได้
  • วิธีสื่อสารกับร้านค้าเพื่อยืนยันการซื้อและจัดเก็บใน Firestore
  • วิธีจัดการการซื้อในแอปของคุณ

สิ่งที่ต้องมี

  • Android Studio 4.1 ขึ้นไป
  • Xcode 12 ขึ้นไป (สำหรับการพัฒนา iOS)
  • Flutter SDK

2. ตั้งค่าสภาพแวดล้อมในการพัฒนาซอฟต์แวร์

หากต้องการเริ่ม Codelab นี้ ให้ดาวน์โหลดโค้ดและเปลี่ยนตัวระบุชุดซอฟต์แวร์สำหรับ iOS และชื่อแพ็กเกจสำหรับ Android

ดาวน์โหลดโค้ด

หากต้องการโคลนที่เก็บ GitHub จากบรรทัดคำสั่ง ให้ใช้คำสั่งต่อไปนี้

git clone https://github.com/flutter/codelabs.git flutter-codelabs

หรือหากติดตั้งเครื่องมือ cli ของ GitHub แล้ว ให้ใช้คำสั่งต่อไปนี้

gh repo clone flutter/codelabs flutter-codelabs

ระบบจะโคลนโค้ดตัวอย่างลงในไดเรกทอรี flutter-codelabs ที่มีโค้ดสำหรับคอลเล็กชัน Codelab โค้ดสำหรับ Codelab นี้คือ flutter-codelabs/in_app_purchases

โครงสร้างไดเรกทอรีภายใต้ flutter-codelabs/in_app_purchases ประกอบด้วยชุดสแนปชอตของตำแหน่งที่คุณควรอยู่ท้ายขั้นตอนที่ตั้งชื่อแต่ละขั้น โค้ดเริ่มต้นอยู่ในขั้นตอนที่ 0 การค้นหาไฟล์ที่ตรงกันจึงง่ายมากดังนี้

cd flutter-codelabs/in_app_purchases/step_00

ถ้าคุณต้องการข้ามไปข้างหน้าหรือดูว่าควรมีลักษณะอย่างไรหลังจากขั้นตอนหนึ่ง ให้ดูในไดเรกทอรีที่ตั้งชื่อตามขั้นตอนที่คุณสนใจ โค้ดของขั้นตอนสุดท้ายอยู่ในโฟลเดอร์ complete

ตั้งค่าโปรเจ็กต์เริ่มต้น

เปิดโปรเจ็กต์เริ่มต้นจาก step_00 ใน IDE ที่คุณชื่นชอบ เราใช้ Android Studio เป็นภาพหน้าจอ แต่โค้ดของ Visual Studio ก็เป็นตัวเลือกที่ยอดเยี่ยมเช่นกัน เมื่อใช้โปรแกรมแก้ไขแบบใดแบบหนึ่ง ให้ตรวจสอบว่าได้ติดตั้งปลั๊กอิน Dart และ Flutter ล่าสุดแล้ว

แอปที่คุณจะต้องใช้ต้องสื่อสารกับ App Store และ Play Store เพื่อให้ทราบว่ามีผลิตภัณฑ์ใดบ้างที่ใช้ได้และมีราคาเท่าใด ทุกแอปจะได้รับการระบุด้วยรหัสที่ไม่ซ้ำกัน สำหรับ App Store ของ iOS รหัสนี้เรียกว่าตัวระบุชุด ส่วนสำหรับ Play Store บน Android รหัสนี้คือรหัสแอปพลิเคชัน ตัวระบุเหล่านี้มักจะสร้างโดยใช้รูปแบบชื่อโดเมนแบบย้อนกลับ ตัวอย่างเช่น เมื่อสร้างแอปการซื้อในแอปสำหรับ flutter.dev เราจะใช้ dev.flutter.inapppurchase ลองนึกถึงตัวระบุสำหรับแอปของคุณ ตอนนี้คุณจะตั้งค่าดังกล่าวในการตั้งค่าโปรเจ็กต์

ขั้นแรก ให้ตั้งค่าตัวระบุกลุ่มสำหรับ iOS

เมื่อเปิดโปรเจ็กต์ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ iOS แล้วคลิก Flutter และเปิดโมดูลในแอป Xcode

942772eb9a73bfaa.png

ในโครงสร้างโฟลเดอร์ของ Xcode มีโปรเจ็กต์ Runner อยู่ที่ด้านบน และเป้าหมาย Flutter, Runner และผลิตภัณฑ์จะอยู่ใต้โปรเจ็กต์ Runner ดับเบิลคลิก Runner เพื่อแก้ไขการตั้งค่าโปรเจ็กต์ แล้วคลิก Signing & ความสามารถ ป้อนตัวระบุชุดซอฟต์แวร์ที่คุณเพิ่งเลือกในช่องทีมเพื่อตั้งค่าทีม

812f919d965c649a.jpeg

คุณสามารถปิด Xcode และกลับไปที่ Android Studio เพื่อกำหนดค่า Android ให้เสร็จได้แล้ว โดยเปิดไฟล์ build.gradle ใน android/app, และเปลี่ยน applicationId (ในบรรทัดที่ 37 ในภาพหน้าจอด้านล่าง) เป็นรหัสแอปพลิเคชัน เช่นเดียวกับตัวระบุ Bundle ของ iOS โปรดทราบว่ารหัสสำหรับร้านค้าบน iOS และ Android ไม่จำเป็นต้องเหมือนกัน แต่รหัสที่เหมือนกันทุกประการจะมีโอกาสเกิดข้อผิดพลาดน้อยกว่า ดังนั้นใน Codelab นี้เราจะใช้ตัวระบุที่เหมือนกันด้วย

5c4733ac560ae8c2.png

3. ติดตั้งปลั๊กอิน

คุณจะต้องติดตั้งปลั๊กอิน in_app_purchase ในส่วนนี้ของ Codelab

เพิ่มทรัพยากร Dependency ใน pubspec

เพิ่ม in_app_purchase ลงใน pubspec โดยการเพิ่ม in_app_purchase ไปยังทรัพยากร Dependency ใน pubspec ของคุณ:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

dependencies:
  ..
  cloud_firestore: ^4.0.3
  firebase_auth: ^4.2.2
  firebase_core: ^2.5.0
  google_sign_in: ^6.0.1
  http: ^0.13.4
  in_app_purchase: ^3.0.1
  intl: ^0.18.0
  provider: ^6.0.2
  ..

คลิก pub get เพื่อดาวน์โหลดแพ็กเกจหรือเรียกใช้ flutter pub get ในบรรทัดคำสั่ง

4. ตั้งค่า App Store

หากต้องการตั้งค่าการซื้อในแอปและทดสอบบน iOS คุณต้องสร้างแอปใหม่ใน App Store และสร้างผลิตภัณฑ์ที่ซื้อได้ในนั้น คุณไม่จำเป็นต้องเผยแพร่อะไรหรือส่งแอปให้ Apple ตรวจสอบเลย คุณต้องมีบัญชีนักพัฒนาซอฟต์แวร์จึงจะดำเนินการนี้ได้ หากยังไม่มี ให้ลงทะเบียนในโปรแกรมนักพัฒนาแอปของ Apple

หากต้องการใช้การซื้อในแอป คุณยังต้องมีข้อตกลงที่มีผลอยู่สำหรับแอปที่ต้องซื้อใน App Store Connect ไปที่ https://appstoreconnect.apple.com/ แล้วคลิกข้อตกลง ภาษี และการธนาคาร

6e373780e5e24a6f.png

คุณจะเห็นข้อตกลงที่นี่สำหรับแอปฟรีและแอปที่ต้องซื้อ สถานะของแอปฟรีควรมีสถานะเป็นใช้งานอยู่ ส่วนแอปที่ต้องซื้อจะเป็นสถานะใหม่ โปรดตรวจสอบว่าคุณดู ยอมรับข้อกำหนด และป้อนข้อมูลที่จำเป็นทั้งหมดแล้ว

74c73197472c9aec.png

เมื่อตั้งค่าทุกอย่างอย่างถูกต้องแล้ว สถานะของแอปที่ต้องซื้อจะทำงาน ข้อนี้สำคัญมากเพราะคุณจะไม่สามารถลองใช้การซื้อในแอปได้หากไม่มีข้อตกลงที่มีผลอยู่

4a100bbb8cafdbbf.jpeg

ลงทะเบียนรหัสแอป

สร้างตัวระบุใหม่ในพอร์ทัลนักพัฒนาซอฟต์แวร์ของ Apple

55d7e592d9a3fc7b.png

เลือกรหัสแอป

13f125598b72ca77.png

เลือกแอป

41ac4c13404e2526.png

ระบุคําอธิบายและตั้งค่ารหัสชุดให้ตรงกับรหัสชุดให้ตรงกับค่าที่ตั้งไว้ก่อนหน้านี้ใน XCode

9d2c940ad80deeef.png

ดูคำแนะนำเพิ่มเติมเกี่ยวกับวิธีสร้างรหัสแอปใหม่ได้ที่ความช่วยเหลือเกี่ยวกับบัญชีนักพัฒนาแอป

การสร้างแอปใหม่

สร้างแอปใหม่ใน App Store Connect โดยใช้รหัสชุดที่ไม่ซ้ำกัน

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

ดูคำแนะนำเพิ่มเติมเกี่ยวกับวิธีสร้างแอปใหม่และจัดการข้อตกลงได้ที่ความช่วยเหลือเกี่ยวกับ App Store Connect

หากต้องการทดสอบการซื้อในแอป คุณต้องมีผู้ใช้ทดสอบแซนด์บ็อกซ์ ผู้ใช้ทดสอบรายนี้ไม่ควรเชื่อมต่อกับ iTunes เนื่องจากใช้สำหรับการทดสอบการซื้อในแอปเท่านั้น คุณไม่สามารถใช้อีเมลที่ใช้กับบัญชี Apple อยู่แล้ว ในส่วนผู้ใช้และการเข้าถึง ให้ไปที่ผู้ทดสอบในส่วนแซนด์บ็อกซ์เพื่อสร้างบัญชีแซนด์บ็อกซ์ใหม่หรือจัดการ Apple ID แซนด์บ็อกซ์ที่มีอยู่

3ca2b26d4e391a4c.jpeg

ขณะนี้คุณสามารถตั้งค่าผู้ใช้แซนด์บ็อกซ์ของคุณบน iPhone ได้โดยไปที่การตั้งค่า > App Store > บัญชีแซนด์บ็อกซ์

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

การกำหนดค่าการซื้อในแอป

ต่อไป คุณจะต้องกำหนดค่ารายการที่ซื้อได้ 3 รายการ ดังนี้

  • dash_consumable_2k: การซื้อสิ้นเปลืองที่ซื้อได้หลายครั้ง ซึ่งให้ขีดกลางยาว 2, 000 ขีด (สกุลเงินในแอป) แก่ผู้ใช้ต่อการซื้อ 1 ครั้ง
  • dash_upgrade_3d: "การอัปเกรด" ที่ใช้ไม่ได้ ที่สามารถซื้อได้เพียงครั้งเดียว และทำให้ผู้ใช้สามารถคลิกดูหน้าแดชบอร์ดที่แตกต่างกันอย่างสวยงาม
  • dash_subscription_doubler: การสมัครใช้บริการที่มอบขีดกลางต่อคลิกให้แก่ผู้ใช้เป็น 2 เท่าตลอดระยะเวลาการสมัครใช้บริการ

d156b2f5bac43ca8.png

ไปที่การซื้อในแอป > จัดการ

สร้างการซื้อในแอปด้วยรหัสที่ระบุ:

  1. ตั้งค่า dash_consumable_2k เป็นอุปกรณ์สิ้นเปลือง

ใช้ dash_consumable_2k เป็นรหัสผลิตภัณฑ์ ชื่อข้อมูลอ้างอิงจะใช้ในการเชื่อมต่อ App Store เท่านั้น เพียงตั้งค่าเป็น dash consumable 2k แล้วเพิ่มการแปลสำหรับการซื้อ เรียกการซื้อด้วย Spring is in the air โดยใช้ 2000 dashes fly out เป็นคำอธิบาย

ec1701834fd8527.png

  1. ตั้งค่า dash_upgrade_3d เป็นอุปกรณ์สิ้นเปลือง

ใช้ dash_upgrade_3d เป็นรหัสผลิตภัณฑ์ ตั้งชื่อข้อมูลอ้างอิงเป็น dash upgrade 3d และเพิ่มการแปลภาษาสำหรับการซื้อ เรียกการซื้อด้วย 3D Dash โดยใช้ Brings your dash back to the future เป็นคำอธิบาย

6765d4b711764c30.png

  1. ตั้งค่า dash_subscription_doubler เป็นการสมัครใช้บริการแบบต่ออายุใหม่อัตโนมัติ

ขั้นตอนในการสมัครใช้บริการจะแตกต่างออกไปเล็กน้อย ก่อนอื่น คุณจะต้องตั้งค่าชื่ออ้างอิงและรหัสผลิตภัณฑ์ ดังนี้

6d29e08dae26a0c4.png

ขั้นตอนต่อไป คุณต้องสร้างกลุ่มการสมัครสมาชิก เมื่อการสมัครใช้บริการหลายรายการอยู่ในกลุ่มเดียวกัน ผู้ใช้จะสมัครใช้บริการรายการใดรายการหนึ่งพร้อมกันได้เท่านั้น แต่จะอัปเกรดหรือดาวน์เกรดระหว่างการสมัครใช้บริการเหล่านี้ได้อย่างง่ายดาย เพียงโทรหากลุ่มนี้ subscriptions

5bd0da17a85ac076.png

จากนั้น ให้ป้อนระยะเวลาการสมัครใช้บริการและการแปล ตั้งชื่อการสมัครใช้บริการนี้ว่า Jet Engine โดยใช้คำอธิบาย Doubles your clicks คลิก Save

bd1b1d82eeee4cb3.png

หลังจากคลิกปุ่มบันทึกแล้ว ให้เพิ่มราคาการสมัครใช้บริการ เลือกราคาที่คุณต้องการ

d0bf39680ef0aa2e.png

ตอนนี้คุณจะเห็นการซื้อ 3 อย่างในรายการการซื้อ

99d5c4b446e8fecf.png

5. ตั้งค่า Play Store

คุณจะต้องมีบัญชีนักพัฒนาแอปสำหรับ Play Store เช่นเดียวกับใน App Store หากยังไม่มีบัญชี ให้ลงทะเบียนบัญชี

สร้างแอปใหม่

สร้างแอปใหม่ใน Google Play Console โดยทำดังนี้

  1. เปิด Play Console
  2. เลือกแอปทั้งหมด > สร้างแอป
  3. เลือกภาษาเริ่มต้นและเพิ่มชื่อแอป พิมพ์ชื่อแอปที่คุณต้องการให้แสดงใน Google Play คุณเปลี่ยนชื่อได้ในภายหลัง
  4. ระบุว่าแอปพลิเคชันของคุณเป็นเกม คุณเปลี่ยนข้อมูลนี้ได้ในภายหลัง
  5. ระบุว่าแอปพลิเคชันของคุณเป็นแบบฟรีหรือต้องซื้อ
  6. เพิ่มอีเมลที่ผู้ใช้ Play Store จะใช้เพื่อติดต่อคุณเกี่ยวกับแอปพลิเคชันนี้ได้
  7. ปฏิบัติตามหลักเกณฑ์ด้านเนื้อหาและประกาศกฎหมายการส่งออกของสหรัฐอเมริกาให้ครบถ้วน
  8. เลือกสร้างแอป

หลังจากสร้างแอปแล้ว ให้ไปที่หน้าแดชบอร์ด แล้วทำงานทั้งหมดให้เสร็จในส่วนตั้งค่าแอป คุณให้ข้อมูลบางอย่างเกี่ยวกับแอป เช่น การจัดประเภทเนื้อหาและภาพหน้าจอ ไว้ที่นี่ 13845badcf9bc1db.png

ลงนามในใบสมัคร

คุณต้องมีบิลด์อย่างน้อย 1 รายการที่อัปโหลดไปยัง Google Play จึงจะทดสอบการซื้อในแอปได้

สำหรับกรณีนี้ คุณต้องรับรองบิลด์รุ่นของคุณด้วยสิ่งอื่นที่ไม่ใช่คีย์การแก้ไขข้อบกพร่อง

สร้างคีย์สโตร์

หากมีคีย์สโตร์อยู่แล้ว ให้ข้ามไปยังขั้นตอนถัดไป หากไม่ใช่ ให้สร้างด้วยการเรียกใช้โค้ดต่อไปนี้ที่บรรทัดคำสั่ง

ใน Mac/Linux ให้ใช้คำสั่งต่อไปนี้

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

ใช้คำสั่งต่อไปนี้ใน Windows

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

คำสั่งนี้จะเก็บไฟล์ key.jks ไว้ในไดเรกทอรีหน้าแรก หากต้องการเก็บไฟล์ไว้ที่อื่น ให้เปลี่ยนอาร์กิวเมนต์ที่คุณส่งไปยังพารามิเตอร์ -keystore รักษา

keystore

ไฟล์ส่วนตัว อย่าตรวจสอบเรื่องนี้ในการควบคุมแหล่งที่มาสาธารณะ

อ้างอิงคีย์สโตร์จากแอป

สร้างไฟล์ชื่อ <your app dir>/android/key.properties ที่มีการอ้างอิงไปยังคีย์สโตร์ของคุณ:

storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>

กำหนดค่าการลงชื่อเข้าใช้ Gradle

กำหนดค่าการลงนามแอปโดยการแก้ไขไฟล์ <your app dir>/android/app/build.gradle

เพิ่มข้อมูลคีย์สโตร์จากไฟล์พร็อพเพอร์ตี้ก่อนบล็อก android:

   def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

   android {
         // omitted
   }

โหลดไฟล์ key.properties ลงในออบเจ็กต์ keystoreProperties

เพิ่มโค้ดต่อไปนี้ก่อนบล็อก buildTypes

   buildTypes {
       release {
           // TODO: Add your own signing config for the release build.
           // Signing with the debug keys for now,
           // so `flutter run --release` works.
           signingConfig signingConfigs.debug
       }
   }

กำหนดค่าบล็อก signingConfigs ในไฟล์ build.gradle ของโมดูลด้วยข้อมูลการกำหนดค่าการรับรองดังนี้

   signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release
       }
   }

ตอนนี้ระบบจะรับรองบิลด์รุ่นของแอปโดยอัตโนมัติ

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการรับรองแอป โปรดดูลงนามแอปใน developer.android.com

อัปโหลดบิลด์แรก

หลังจากกำหนดค่าแอปสำหรับการรับรองแล้ว คุณควรสร้างแอปพลิเคชันได้โดยเรียกใช้

flutter build appbundle

คำสั่งนี้จะสร้างบิลด์รุ่นโดยค่าเริ่มต้นและดูเอาต์พุตได้ที่ <your app dir>/build/app/outputs/bundle/release/

จากหน้าแดชบอร์ดใน Google Play Console ให้ไปที่รุ่น > ทดสอบ > การทดสอบแบบปิด และสร้างรุ่นการทดสอบแบบปิดใหม่

สำหรับ Codelab นี้ คุณจะใช้ Google Signing ในแอปต่อไป ดังนั้นให้ดำเนินการต่อและกดดำเนินการต่อใต้ Play App Signing เพื่อเลือกใช้

ba98446d9c5c40e0.png

จากนั้นอัปโหลด App Bundle app-release.aab ที่สร้างขึ้นโดยคำสั่งบิลด์

คลิกบันทึก แล้วคลิกตรวจสอบรุ่น

สุดท้าย ให้คลิกเริ่มเปิดตัวในการทดสอบภายในเพื่อเปิดใช้งานรุ่นการทดสอบภายใน

ตั้งค่าผู้ใช้ทดสอบ

หากต้องการทดสอบการซื้อในแอป จะต้องเพิ่มบัญชี Google ของผู้ทดสอบใน Google Play Console จาก 2 ที่ต่อไปนี้

  1. ไปยังแทร็กทดสอบที่เฉพาะเจาะจง (การทดสอบภายใน)
  2. ในฐานะผู้ทดสอบใบอนุญาต

ก่อนอื่น ให้เริ่มจากการเพิ่มผู้ทดสอบลงในแทร็กทดสอบภายใน กลับไปที่รุ่น > ทดสอบ > การทดสอบภายใน แล้วคลิกแท็บผู้ทดสอบ

a0d0394e85128f84.png

สร้างรายชื่ออีเมลใหม่โดยคลิกสร้างรายชื่ออีเมล ตั้งชื่อรายการ และเพิ่มอีเมลของบัญชี Google ที่ต้องเข้าถึงการทดสอบการซื้อในแอป

จากนั้นเลือกช่องทำเครื่องหมายของรายการ และคลิกบันทึกการเปลี่ยนแปลง

จากนั้นเพิ่มผู้ทดสอบใบอนุญาตโดยทำดังนี้

  1. กลับไปที่มุมมองแอปทั้งหมดของ Google Play Console
  2. ไปที่การตั้งค่า > การทดสอบใบอนุญาต
  3. เพิ่มอีเมลเดียวกันของผู้ทดสอบที่ต้องทดสอบการซื้อในแอปได้
  4. ตั้งค่าการตอบกลับเกี่ยวกับใบอนุญาตเป็น RESPOND_NORMALLY
  5. คลิกบันทึกการเปลี่ยนแปลง

a1a0f9d3e55ea8da.png

การกำหนดค่าการซื้อในแอป

ต่อไปนี้ คุณจะต้องกำหนดค่ารายการที่สามารถซื้อภายในแอปได้

เช่นเดียวกับใน App Store คุณจะต้องกำหนดการซื้อที่แตกต่างกัน 3 รายการดังนี้

  • dash_consumable_2k: การซื้อสิ้นเปลืองที่ซื้อได้หลายครั้ง ซึ่งให้ขีดกลางยาว 2, 000 ขีด (สกุลเงินในแอป) แก่ผู้ใช้ต่อการซื้อ 1 ครั้ง
  • dash_upgrade_3d: "การอัปเกรด" ที่ใช้ไม่ได้ ที่สามารถซื้อได้เพียงครั้งเดียว ซึ่งทำให้ผู้ใช้คลิกได้บน Dash ที่แตกต่างกันอย่างสวยงาม
  • dash_subscription_doubler: การสมัครใช้บริการที่มอบขีดกลางต่อคลิกให้แก่ผู้ใช้เป็น 2 เท่าตลอดระยะเวลาการสมัครใช้บริการ

ขั้นแรก ให้เพิ่มรายการโฆษณาที่ใช้แล้วหมดไปแล้วและไม่ใช่ประเภทที่ใช้แล้วหมดไป

  1. ไปที่ Google Play Console แล้วเลือกแอปพลิเคชันของคุณ
  2. ไปที่สร้างรายได้ > ผลิตภัณฑ์ > ไอเทมที่ซื้อในแอป
  3. คลิกสร้างผลิตภัณฑ์c8d66e32f57dee21.png
  4. ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับผลิตภัณฑ์ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณต้องการใช้ทั้งหมด
  5. คลิกบันทึก
  6. คลิกเปิดใช้งาน
  7. ทำขั้นตอนเดียวกันนี้อีกครั้งสำหรับ "การอัปเกรด" เวอร์ชันที่ใช้งานจริง การซื้อ

ถัดไป ให้เพิ่มการสมัครรับข้อมูลดังนี้

  1. ไปที่ Google Play Console แล้วเลือกแอปพลิเคชันของคุณ
  2. ไปที่สร้างรายได้ > ผลิตภัณฑ์ > การสมัครใช้บริการ
  3. คลิกสร้างการสมัครใช้บริการ32a6a9eefdb71dd0.png
  4. ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับการสมัครใช้บริการ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณต้องการใช้ทั้งหมด
  5. คลิกบันทึก

ตอนนี้คุณควรตั้งค่าการซื้อใน Play Console แล้ว

6. ตั้งค่า Firebase

ใน Codelab นี้ คุณจะใช้บริการแบ็กเอนด์เพื่อยืนยันและติดตามผู้ใช้ การซื้อ

การใช้บริการแบ็กเอนด์มีประโยชน์หลายประการดังนี้

  • คุณสามารถยืนยันธุรกรรมได้อย่างปลอดภัย
  • คุณตอบสนองต่อเหตุการณ์การเรียกเก็บเงินจาก App Store ได้
  • คุณสามารถติดตามการซื้อได้ในฐานข้อมูล
  • ผู้ใช้จะไม่สามารถหลอกให้แอปของคุณให้บริการฟีเจอร์พรีเมียมด้วยการกรอกลับนาฬิกาของระบบ

การตั้งค่าบริการแบ็กเอนด์มีหลายวิธี แต่คุณจะทำได้โดยใช้ฟังก์ชันระบบคลาวด์และ Firestore โดยใช้ Firebase ของ Google เอง

การเขียนแบ็กเอนด์ถือว่าอยู่นอกขอบเขตของ Codelab นี้ โค้ดเริ่มต้นจึงมีโปรเจ็กต์ Firebase ที่จัดการการซื้อพื้นฐานอยู่แล้วเพื่อให้คุณเริ่มต้นใช้งาน

ปลั๊กอิน Firebase รวมอยู่ในแอปเริ่มต้นด้วย

สิ่งสุดท้ายที่คุณต้องดำเนินการก็คือการสร้างโปรเจ็กต์ Firebase ของคุณเอง กำหนดค่าทั้งแอปและแบ็กเอนด์สำหรับ Firebase จากนั้นจึงทำให้แบ็กเอนด์ใช้งานได้

สร้างโปรเจ็กต์ Firebase

ไปที่คอนโซล Firebase แล้วสร้างโปรเจ็กต์ Firebase ใหม่ สำหรับตัวอย่างนี้ ให้เรียกโปรเจ็กต์ Dash Clicker

ในแอปแบ็กเอนด์ คุณจะต้องผูกการซื้อเข้ากับผู้ใช้ที่เจาะจง คุณจึงต้องมีการตรวจสอบสิทธิ์ ในกรณีนี้ ให้ใช้ประโยชน์จากโมดูลการตรวจสอบสิทธิ์ของ Firebase ด้วย Google Sign-In

  1. จากหน้าแดชบอร์ด Firebase ให้ไปที่การตรวจสอบสิทธิ์และเปิดใช้หากจำเป็น
  2. ไปที่แท็บวิธีการลงชื่อเข้าใช้ และเปิดใช้ผู้ให้บริการการลงชื่อเข้าใช้ Google

7babb48832fbef29

โปรดเปิดใช้ด้วยเนื่องจากคุณจะใช้ฐานข้อมูล Firestore ของ Firebase ด้วย

e20553e0de5ac331.png

ตั้งกฎ Cloud Firestore ดังนี้

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /purchases/{purchaseId} {
      allow read: if request.auth != null && request.auth.uid == resource.data.userId
    }
  }
}

ตั้งค่า Firebase สำหรับ Flutter

วิธีที่แนะนำสำหรับการติดตั้ง Firebase ในแอป Flutter คือการใช้ FlutterFire CLI ทำตามวิธีการตามที่อธิบายไว้ในหน้าการตั้งค่า

เมื่อเรียกใช้การกำหนดค่า Flutterfire ให้เลือกโปรเจ็กต์ที่คุณเพิ่งสร้างในขั้นตอนก่อนหน้า

$ flutterfire configure

i Found 5 Firebase projects.                                                                                                  
? Select a Firebase project to configure your Flutter application with ›                                                      
❯ in-app-purchases-1234 (in-app-purchases-1234)                                                                         
  other-flutter-codelab-1 (other-flutter-codelab-1)                                                                           
  other-flutter-codelab-2 (other-flutter-codelab-2)                                                                      
  other-flutter-codelab-3 (other-flutter-codelab-3)                                                                           
  other-flutter-codelab-4 (other-flutter-codelab-4)                                                                                                                                                               
  <create a new project>  

จากนั้นเปิดใช้ iOS และ Android โดยเลือก 2 แพลตฟอร์ม

? Which platforms should your configuration support (use arrow keys & space to select)? ›                                     
✔ android                                                                                                                     
✔ ios                                                                                                                         
  macos                                                                                                                       
  web                                                                                                                          

เมื่อมีข้อความแจ้งเกี่ยวกับการลบล้าง firebase_options.dart ให้เลือก "ใช่"

? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes                                                                                                                         

ตั้งค่า Firebase สำหรับ Android: ขั้นตอนถัดไป

จากหน้าแดชบอร์ด Firebase ให้ไปที่ภาพรวมโครงการ เลือกการตั้งค่า แล้วเลือกแท็บทั่วไป

เลื่อนลงไปที่แอปของคุณ แล้วเลือกแอป dashclicker (android)

b22d46a759c0c834.png

หากต้องการอนุญาตให้ Google Sign-In ในโหมดแก้ไขข้อบกพร่อง คุณต้องระบุแฮชลายนิ้วมือ SHA-1 ของใบรับรองการแก้ไขข้อบกพร่อง

รับแฮชใบรับรองที่มีการรับรองเพื่อแก้ไขข้อบกพร่อง

ในรูทของโปรเจ็กต์แอป Flutter ให้เปลี่ยนไดเรกทอรีเป็นโฟลเดอร์ android/ แล้วสร้างรายงานการรับรอง

cd android
./gradlew :app:signingReport

คุณจะเห็นรายการคีย์ Signing จำนวนมาก เนื่องจากคุณกำลังมองหาแฮชของใบรับรองการแก้ไขข้อบกพร่อง ให้มองหาใบรับรองที่มีการตั้งค่าพร็อพเพอร์ตี้ Variant และ Config เป็น debug คีย์สโตร์อาจอยู่ในโฟลเดอร์หน้าแรกภายใต้ .android/debug.keystore

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

คัดลอกแฮช SHA-1 แล้วกรอกข้อมูลในช่องสุดท้ายในกล่องโต้ตอบโมดัลการส่งแอป

ตั้งค่า Firebase สำหรับ iOS: ขั้นตอนถัดไป

เปิด ios/Runnder.xcworkspace ด้วย Xcode หรือ IDE ที่คุณเลือกก็ได้

ใน VSCode ให้คลิกขวาที่โฟลเดอร์ ios/ แล้วคลิก open in xcode

ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ ios/ จากนั้นคลิก flutter ตามด้วยตัวเลือก open iOS module in Xcode

หากต้องการให้ Google Sign-In ใน iOS ได้ ให้เพิ่มตัวเลือกการกำหนดค่า CFBundleURLTypes ลงในไฟล์บิลด์ plist (โปรดดูข้อมูลเพิ่มเติมในเอกสารแพ็กเกจ google_sign_in) ในกรณีนี้ ไฟล์จะเป็น ios/Runner/Info-Debug.plist และ ios/Runner/Info-Release.plist

เพิ่มคู่คีย์-ค่าแล้ว แต่ต้องแทนที่ค่าของคู่คีย์-ค่า

  1. รับค่าของ REVERSED_CLIENT_ID จากไฟล์ GoogleService-Info.plist โดยไม่มีองค์ประกอบ <string>..</string> อยู่ล้อมรอบ
  2. แทนที่ค่าทั้งในไฟล์ ios/Runner/Info-Debug.plist และ ios/Runner/Info-Release.plist ด้วยคีย์ CFBundleURLTypes
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.REDACTED</string>
        </array>
    </dict>
</array>

ตอนนี้คุณตั้งค่า Firebase เสร็จเรียบร้อยแล้ว

7. ฟังข้อมูลอัปเดตเกี่ยวกับการซื้อ

ในส่วนนี้ คุณจะได้เตรียมแอปสำหรับการซื้อผลิตภัณฑ์ ขั้นตอนนี้จะรวมถึงการฟังการอัปเดตการซื้อและข้อผิดพลาดหลังจากที่แอปเริ่มทำงาน

ฟังข้อมูลอัปเดตเกี่ยวกับการซื้อ

ใน main.dart, ให้ค้นหาวิดเจ็ต MyHomePage ที่มี Scaffold ที่มี BottomNavigationBar ที่มี 2 หน้า หน้านี้ยังสร้าง Provider 3 รายการสำหรับ DashCounter, DashUpgrades, และ DashPurchases DashCounter จะติดตามจํานวนขีดกลางในปัจจุบันและจะเพิ่มขีดกลางโดยอัตโนมัติ DashUpgrades จัดการการอัปเกรดที่คุณซื้อได้ด้วยขีดกลาง Codelab นี้มุ่งเน้นที่ DashPurchases

โดยค่าเริ่มต้น ระบบจะกำหนดออบเจ็กต์ของผู้ให้บริการเมื่อมีการขอออบเจ็กต์ดังกล่าวเป็นครั้งแรก ออบเจ็กต์นี้จะรอฟังการอัปเดตการซื้อโดยตรงเมื่อแอปเริ่มทำงาน ดังนั้นให้ปิดใช้การโหลดแบบ Lazy Loading ในออบเจ็กต์นี้ด้วย lazy: false:

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create: (context) => DashPurchases(
    context.read<DashCounter>(),
  ),
  lazy: false,
),

คุณต้องมีอินสแตนซ์ของ InAppPurchaseConnection ด้วย อย่างไรก็ตาม หากต้องการทดสอบแอปอยู่เสมอ คุณจะต้องมีวิธีการจำลองการเชื่อมต่อ ซึ่งทำได้โดยสร้างเมธอดของอินสแตนซ์ที่ลบล้างได้ในการทดสอบ แล้วเพิ่มไปยัง main.dart

lib/main.dart

// Gives the option to override in tests.
class IAPConnection {
  static InAppPurchase? _instance;
  static set instance(InAppPurchase value) {
    _instance = value;
  }

  static InAppPurchase get instance {
    _instance ??= InAppPurchase.instance;
    return _instance!;
  }
}

คุณต้องอัปเดตการทดสอบเล็กน้อยหากต้องการให้การทดสอบทำงานต่อไป ดู widget_test.dart บน GitHub สำหรับโค้ดแบบเต็มสำหรับ TestIAPConnection

test/widget_test.dart

void main() {
  testWidgets('App starts', (WidgetTester tester) async {
    IAPConnection.instance = TestIAPConnection();
    await tester.pumpWidget(const MyApp());
    expect(find.text('Tim Sneath'), findsOneWidget);
  });
}

ใน lib/logic/dash_purchases.dart ให้ไปที่รหัสของ DashPurchases ChangeNotifier ปัจจุบันมีเพียง DashCounter ที่คุณสามารถเพิ่มลงในขีดกลางที่ซื้อได้

เพิ่มพร็อพเพอร์ตี้การสมัครใช้บริการสตรีม _subscription (ประเภท StreamSubscription<List<PurchaseDetails>> _subscription;) IAPConnection.instance, และการนําเข้า โค้ดที่ได้ควรมีลักษณะดังนี้

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

class DashPurchases extends ChangeNotifier {
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter);
}

ระบบเพิ่มคีย์เวิร์ด late ลงใน _subscription เนื่องจาก _subscription ได้รับการเริ่มต้นในตัวสร้าง โปรเจ็กต์นี้มีการตั้งค่าให้ไม่สามารถเว้นว่างได้โดยค่าเริ่มต้น (NNBD) ซึ่งหมายความว่าพร็อพเพอร์ตี้ที่ไม่ได้ประกาศเป็น Null ต้องมีค่าที่ไม่เป็น Null ตัวระบุ late ช่วยให้คุณกำหนดค่านี้ล่าช้าได้

ในเครื่องมือสร้าง ให้ติดตั้ง purchaseUpdatedStream และเริ่มฟังสตรีม ในเมธอด dispose() ให้ยกเลิกการสมัครใช้บริการสตรีม

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter) {
    final purchaseUpdated =
        iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  Future<void> buy(PurchasableProduct product) async {
    // omitted
  }

  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    //Handle error here
  }
}

ตอนนี้แอปได้รับการอัปเดตการซื้อแล้ว ดังนั้นในส่วนถัดไป คุณจะได้ทำการซื้อ

ก่อนดำเนินการต่อ ให้ทดสอบกับ "flutter test"" เพื่อยืนยันว่าตั้งค่าทุกอย่างถูกต้อง

$ flutter test

00:01 +1: All tests passed!                                                                                   

8. ซื้อสินค้าหรือบริการ

ในส่วนนี้ คุณจะแทนที่ผลิตภัณฑ์จำลองที่มีอยู่ด้วยผลิตภัณฑ์ที่ซื้อได้จริง ผลิตภัณฑ์เหล่านี้โหลดจากร้านค้าซึ่งแสดงในรายการและจะซื้อเมื่อแตะผลิตภัณฑ์

ปรับผลิตภัณฑ์ที่ซื้อได้

PurchasableProduct แสดงผลิตภัณฑ์จำลอง โปรดอัปเดตโค้ดเพื่อแสดงเนื้อหาจริงโดยแทนที่คลาส PurchasableProduct ใน purchasable_product.dart ด้วยโค้ดต่อไปนี้

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus {
  purchasable,
  purchased,
  pending,
}

class PurchasableProduct {
  String get id => productDetails.id;
  String get title => productDetails.title;
  String get description => productDetails.description;
  String get price => productDetails.price;
  ProductStatus status;
  ProductDetails productDetails;

  PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}

ใน dash_purchases.dart, ให้นำการซื้อจำลองออกและแทนที่ด้วยรายการที่ว่างเปล่า List<PurchasableProduct> products = [];

โหลดรายการที่ซื้อได้

โหลดการซื้อจากร้านค้าเพื่อให้ผู้ใช้ทำการซื้อได้ ก่อนอื่น ให้ตรวจสอบว่าร้านค้าดังกล่าวพร้อมให้บริการหรือไม่ เมื่อร้านค้าไม่พร้อมให้บริการ การตั้งค่า storeState เป็น notAvailable จะแสดงข้อความแสดงข้อผิดพลาดให้แก่ผู้ใช้

lib/logic/dash_purchases.dart

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
  }

เมื่อร้านค้าพร้อมให้บริการ ให้โหลดรายการที่ซื้อที่ใช้ได้ จากการตั้งค่า Firebase ก่อนหน้านี้ คุณจะเห็น storeKeyConsumable, storeKeySubscription, และ storeKeyUpgrade เมื่อไม่มีการซื้อที่คาดไว้ ให้พิมพ์ข้อมูลนี้ไปยังคอนโซล เราขอแนะนำให้คุณส่งข้อมูลนี้ไปยังบริการแบ็กเอนด์ด้วย

เมธอด await iapConnection.queryProductDetails(ids) จะแสดงทั้งรหัสที่ไม่พบและผลิตภัณฑ์ที่ซื้อได้ที่พบ ใช้ productDetails จากคำตอบเพื่ออัปเดต UI และตั้งค่า StoreState เป็น available

lib/logic/dash_purchases.dart

import '../constants.dart';

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

เรียกฟังก์ชัน loadPurchases() ในเครื่องมือสร้างดังนี้

lib/logic/dash_purchases.dart

  DashPurchases(this.counter) {
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    loadPurchases();
  }

สุดท้าย เปลี่ยนค่าของช่อง storeState จาก StoreState.available เป็น StoreState.loading:

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

แสดงผลิตภัณฑ์ที่ซื้อได้

และลองใช้ไฟล์ purchase_page.dart วิดเจ็ต PurchasePage จะแสดง _PurchasesLoading, _PurchaseList, หรือ _PurchasesNotAvailable, ขึ้นอยู่กับ StoreState วิดเจ็ตยังแสดงการซื้อที่ผ่านมาของผู้ใช้ซึ่งจะใช้ในขั้นตอนถัดไปด้วย

วิดเจ็ต _PurchaseList จะแสดงรายการผลิตภัณฑ์ที่ซื้อได้และส่งคำขอซื้อไปยังออบเจ็กต์ DashPurchases

lib/pages/purchase_page.dart

class _PurchaseList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var purchases = context.watch<DashPurchases>();
    var products = purchases.products;
    return Column(
      children: products
          .map((product) => _PurchaseWidget(
              product: product,
              onPressed: () {
                purchases.buy(product);
              }))
          .toList(),
    );
  }
}

คุณควรเห็นผลิตภัณฑ์ที่พร้อมจำหน่ายในร้านค้า Android และ iOS หากกำหนดค่าอย่างถูกต้อง โปรดทราบว่าอาจใช้เวลาสักครู่ก่อนที่รายการที่ซื้อจะพร้อมใช้งานเมื่อป้อนลงในคอนโซลที่เกี่ยวข้อง

ca1a9f97c21e552d.png

กลับไปที่ dash_purchases.dart แล้วใช้ฟังก์ชันเพื่อซื้อผลิตภัณฑ์ คุณแค่ต้องแยกสินค้าอุปโภคบริโภคออกจากสินค้าบริโภคเท่านั้น การอัปเกรดและผลิตภัณฑ์ที่ต้องสมัครใช้บริการนั้นใช้งานไม่ได้

lib/logic/dash_purchases.dart

  Future<void> buy(PurchasableProduct product) async {
    final purchaseParam = PurchaseParam(productDetails: product.productDetails);
    switch (product.id) {
      case storeKeyConsumable:
        await iapConnection.buyConsumable(purchaseParam: purchaseParam);
        break;
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      default:
        throw ArgumentError.value(
            product.productDetails, '${product.id} is not a known product');
    }
  }

ก่อนดำเนินการต่อ ให้สร้างตัวแปร _beautifiedDashUpgrade และอัปเดต Getter ของ beautifiedDash เพื่ออ้างอิง

lib/logic/dash_purchases.dart

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;

เมธอด _onPurchaseUpdate จะได้รับการอัปเดตการซื้อ อัปเดตสถานะของผลิตภัณฑ์ที่แสดงในหน้าการซื้อ และนำการซื้อไปใช้กับตรรกะตัวนับ คุณจะต้องโทรติดต่อ completePurchase หลังจากจัดการการซื้อเพื่อให้ร้านค้าทราบว่าการซื้อได้รับการจัดการอย่างถูกต้อง

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

9. ตั้งค่าแบ็กเอนด์

ก่อนจะไปต่อที่การติดตามและยืนยันการซื้อ ให้ตั้งค่าแบ็กเอนด์ของ DART เพื่อรองรับการดำเนินการดังกล่าว

ในส่วนนี้ ให้ดำเนินการจากโฟลเดอร์ dart-backend/ ในฐานะรูท

ตรวจสอบว่าคุณได้ติดตั้งเครื่องมือต่อไปนี้แล้ว

ภาพรวมโปรเจ็กต์พื้นฐาน

เนื่องจากบางส่วนของโครงการนี้ถือว่าอยู่นอกขอบเขตของ Codelab นี้ จึงรวมอยู่ในโค้ดเริ่มต้น คุณควรทบทวนสิ่งที่อยู่ในโค้ดเริ่มต้นอยู่แล้วก่อนที่จะเริ่มต้น เพื่อให้ทราบว่าคุณจะวางโครงสร้างของสิ่งต่างๆ อย่างไร

โค้ดแบ็กเอนด์นี้สามารถเรียกใช้ภายในเครื่องของคุณได้ คุณไม่จำเป็นต้องติดตั้งใช้งานโค้ด อย่างไรก็ตาม คุณจำเป็นต้องสามารถเชื่อมต่อจากอุปกรณ์การพัฒนา (Android หรือ iPhone) ของคุณเข้ากับเครื่องที่เซิร์ฟเวอร์จะทำงาน โดยเครื่องจะต้องอยู่ในเครือข่ายเดียวกัน และคุณจำเป็นต้องทราบที่อยู่ IP ของเครื่อง

ลองเรียกใช้เซิร์ฟเวอร์ด้วยคำสั่งต่อไปนี้

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

แบ็กเอนด์ของ DART ใช้ shelf และ shelf_router เพื่อแสดงปลายทาง API โดยค่าเริ่มต้น เซิร์ฟเวอร์จะไม่ให้เส้นทางใดๆ หลังจากนั้น คุณจะสร้างเส้นทางเพื่อจัดการกระบวนการยืนยันการซื้อ

ส่วนหนึ่งที่รวมอยู่ในโค้ดเริ่มต้นอยู่แล้วคือ IapRepository ใน lib/iap_repository.dart เนื่องจากการเรียนรู้วิธีโต้ตอบกับ Firestore หรือฐานข้อมูลโดยทั่วไปไม่ถือว่าเกี่ยวข้องกับ Codelab นี้ โค้ดเริ่มต้นจึงมีฟังก์ชันให้คุณสร้างหรืออัปเดตการซื้อใน Firestore รวมถึงคลาสทั้งหมดสำหรับการซื้อเหล่านั้น

ตั้งค่าการเข้าถึง Firebase

หากต้องการเข้าถึง Firebase Firestore คุณต้องมีคีย์สิทธิ์เข้าถึงบัญชีบริการ สร้าง 1 รายการโดยเปิดการตั้งค่าโปรเจ็กต์ Firebase แล้วไปที่ส่วนบัญชีบริการ จากนั้นเลือกสร้างคีย์ส่วนตัวใหม่

27590fc77ae94ad4.png

คัดลอกไฟล์ JSON ที่ดาวน์โหลดไปยังโฟลเดอร์ assets/ และเปลี่ยนชื่อเป็น service-account-firebase.json

ตั้งค่าการเข้าถึง Google Play

หากต้องการเข้าถึง Play Store เพื่อยืนยันการซื้อ คุณต้องสร้างบัญชีบริการที่มีสิทธิ์เหล่านี้และดาวน์โหลดข้อมูลเข้าสู่ระบบ JSON สำหรับบัญชีดังกล่าว

  1. ไปที่ Google Play Console แล้วเริ่มจากหน้าแอปทั้งหมด
  2. ไปที่การตั้งค่า > การเข้าถึง API 317fdfb54921f50e.png ในกรณีที่ Google Play Console ขอให้คุณสร้างหรือลิงก์กับโปรเจ็กต์ที่มีอยู่ ให้ทำก่อนแล้วจึงกลับมาที่หน้านี้
  3. ค้นหาส่วนที่คุณกำหนดบัญชีบริการ แล้วคลิกสร้างบัญชีบริการใหม่1e70d3f8d794bebb.png
  4. คลิกลิงก์ Google Cloud Platform ในกล่องโต้ตอบที่ปรากฏขึ้น 7c9536336dd9e9b4.png
  5. เลือกโปรเจ็กต์ หากไม่เห็น ให้ตรวจสอบว่าคุณได้ลงชื่อเข้าใช้บัญชี Google ที่ถูกต้องในรายการแบบเลื่อนลงบัญชีที่ด้านบนขวา 3fb3a25bad803063.png
  6. หลังจากเลือกโปรเจ็กต์แล้ว ให้คลิก + สร้างบัญชีบริการในแถบเมนูด้านบน 62fe4c3f8644acd8.png
  7. ระบุชื่อบัญชีบริการ ใส่คำอธิบายหรือไม่ก็ได้ เพื่อให้คุณจดจำได้สำหรับบัญชีบริการ แล้วไปยังขั้นตอนถัดไป 8a92d5d6a3dff48c.png
  8. มอบหมายบทบาทผู้แก้ไขให้กับบัญชีบริการ 6052b7753667ed1a.png
  9. ดำเนินการวิซาร์ดให้เสร็จ แล้วกลับไปที่หน้าการเข้าถึง API ใน Developer Console แล้วคลิกรีเฟรชบัญชีบริการ คุณจะเห็นบัญชีที่สร้างใหม่ในรายการ 5895a7db8b4c7659.png
  10. คลิกให้สิทธิ์เข้าถึงสำหรับบัญชีบริการใหม่
  11. เลื่อนลงในหน้าถัดไปที่บล็อกข้อมูลทางการเงิน เลือกทั้งดูข้อมูลทางการเงิน คำสั่งซื้อ และการตอบแบบสำรวจการยกเลิกและจัดการคำสั่งซื้อและการสมัครใช้บริการ 75b22d0201cf67e.png
  12. คลิกเชิญผู้ใช้ 70ea0b1288c62a59.png
  13. หลังจากที่ตั้งค่าบัญชีแล้ว คุณก็เพียงแค่สร้างข้อมูลเข้าสู่ระบบบางอย่าง กลับไปที่ Cloud Console ค้นหาบัญชีบริการในรายการบัญชีบริการ คลิกจุดแนวตั้ง 3 จุด แล้วเลือกจัดการคีย์ 853ee186b0e9954e.png
  14. สร้างคีย์ JSON ใหม่และดาวน์โหลด 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. เปลี่ยนชื่อไฟล์ที่ดาวน์โหลดเป็น service-account-google-play.json, แล้วย้ายไปยังไดเรกทอรี assets/

อีกสิ่งหนึ่งที่เราต้องทำคือเปิด lib/constants.dart, และแทนที่ค่าของ androidPackageId ด้วยรหัสแพ็กเกจที่คุณเลือกสำหรับแอป Android

ตั้งค่าการเข้าถึง Apple App Store

หากต้องการเข้าถึง App Store เพื่อยืนยันการซื้อ คุณต้องตั้งค่าข้อมูลลับที่ใช้ร่วมกันดังนี้

  1. เปิด App Store Connect
  2. ไปที่แอปของฉัน แล้วเลือกแอป
  3. ในแถบนำทางด้านข้าง ให้ไปที่การซื้อในแอป > จัดการ
  4. คลิก App-Specific Shared Secret ที่ด้านขวาบนของรายการ
  5. สร้างข้อมูลลับใหม่แล้วคัดลอก
  6. เปิด lib/constants.dart, และแทนที่ค่า appStoreSharedSecret ด้วยข้อมูลลับที่ใช้ร่วมกันที่คุณเพิ่งสร้าง

d8b8042470aaeff.png

b72f4565750e2f40.png

ไฟล์การกำหนดค่าค่าคงที่

ก่อนดำเนินการต่อ โปรดตรวจสอบว่ามีการกำหนดค่าคงที่ต่อไปนี้ในไฟล์ lib/constants.dart

  • androidPackageId: รหัสแพ็กเกจที่ใช้ใน Android เช่น วันที่ com.example.dashclicker
  • appStoreSharedSecret: ข้อมูลลับที่แชร์ในการเข้าถึง App Store Connect เพื่อทำการยืนยันการซื้อ
  • bundleId: รหัสชุดที่ใช้ใน iOS เช่น วันที่ com.example.dashclicker

ในตอนนี้คุณไม่จำเป็นต้องสนใจค่าคงที่อื่นๆ ก็ได้

10. ยืนยันการซื้อ

ขั้นตอนทั่วไปในการยืนยันการซื้อจะคล้ายกันสำหรับ iOS และ Android

สำหรับร้านค้าทั้ง 2 แห่ง แอปพลิเคชันของคุณจะได้รับโทเค็นเมื่อทำการซื้อ

แอปจะส่งโทเค็นนี้ไปยังบริการแบ็กเอนด์ของคุณ ซึ่งจะยืนยันการซื้อกับเซิร์ฟเวอร์ของร้านค้าที่เกี่ยวข้องโดยใช้โทเค็นที่ให้ไว้

จากนั้นบริการแบ็กเอนด์สามารถเลือกที่จะจัดเก็บการซื้อ และตอบกลับแอปพลิเคชันว่าการซื้อนี้ถูกต้องหรือไม่

การให้บริการแบ็กเอนด์ทำการตรวจสอบกับร้านค้าแทนให้แอปพลิเคชันทำงานบนอุปกรณ์ของผู้ใช้ คุณสามารถป้องกันไม่ให้ผู้ใช้เข้าถึงฟีเจอร์พรีเมียมได้ด้วยการกรอกลับนาฬิการะบบ

ตั้งค่าด้าน Flutter

ตั้งค่าการตรวจสอบสิทธิ์

เมื่อคุณจะส่งรายการที่ซื้อไปยังบริการแบ็กเอนด์ คุณควรตรวจสอบว่าผู้ใช้ได้รับการตรวจสอบสิทธิ์ขณะทำการซื้อ ระบบเพิ่มตรรกะการตรวจสอบสิทธิ์ส่วนใหญ่ไว้แล้วในโปรเจ็กต์เริ่มต้น คุณเพียงแค่ต้องตรวจสอบว่า PurchasePage แสดงปุ่มเข้าสู่ระบบเมื่อผู้ใช้ยังไม่ได้เข้าสู่ระบบ เพิ่มโค้ดต่อไปนี้ลงในจุดเริ่มต้นของเมธอดบิลด์ของ PurchasePage

lib/pages/purchase_page.dart

import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

class PurchasePage extends StatelessWidget {  
  const PurchasePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var firebaseNotifier = context.watch<FirebaseNotifier>();
    if (firebaseNotifier.state == FirebaseState.loading) {
      return _PurchasesLoading();
    } else if (firebaseNotifier.state == FirebaseState.notAvailable) {
      return _PurchasesNotAvailable();
    }

    if (!firebaseNotifier.loggedIn) {
      return const LoginPage();
    }
    // omitted

ปลายทางการยืนยันการโทรจากแอป

ในแอป ให้สร้างฟังก์ชัน _verifyPurchase(PurchaseDetails purchaseDetails) ที่เรียกใช้ปลายทาง /verifypurchase บนแบ็กเอนด์ของ DART โดยใช้การเรียกหลัง HTTP

ส่ง Store ที่เลือก (google_play สำหรับ Play Store หรือ app_store สำหรับ App Store), serverVerificationData และ productID เซิร์ฟเวอร์จะแสดงรหัสสถานะที่ระบุว่าการซื้อได้รับการยืนยันแล้วหรือไม่

ในการกำหนดค่าคงที่ของแอป ให้กำหนดค่า IP ของเซิร์ฟเวอร์ไปยังที่อยู่ IP ของเครื่องภายใน

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

  DashPurchases(this.counter, this.firebaseNotifier) {
    // omitted
  }

เพิ่ม firebaseNotifier ที่มีการสร้าง DashPurchases ใน main.dart:

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
          ),
          lazy: false,
        ),

เพิ่ม Getter สำหรับผู้ใช้ใน FirebaseNotifier เพื่อให้คุณสามารถส่งต่อ User ID ไปยังฟังก์ชันยืนยันการซื้อ

lib/logic/firebase_notifier.dart

  User? get user => FirebaseAuth.instance.currentUser;

เพิ่มฟังก์ชัน _verifyPurchase ในคลาส DashPurchases ฟังก์ชัน async นี้จะแสดงบูลีนที่ระบุว่าการซื้อได้รับการตรวจสอบแล้วหรือไม่

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

เรียกใช้ฟังก์ชัน _verifyPurchase ใน _handlePurchase ก่อนที่จะใช้การซื้อ คุณควรใช้การซื้อเมื่อการซื้อได้รับการยืนยันแล้วเท่านั้น ในแอปเวอร์ชันที่ใช้งานจริง คุณจะระบุเพิ่มเติมได้อีก เช่น ใช้การสมัครใช้บริการแบบทดลองใช้เมื่อ Store ไม่พร้อมให้บริการชั่วคราว อย่างไรก็ตาม สำหรับตัวอย่างนี้ เรียบง่ายและใช้การซื้อเฉพาะเมื่อการซื้อได้รับการยืนยันเรียบร้อยแล้วเท่านั้น

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      // Send to server
      var validPurchase = await _verifyPurchase(purchaseDetails);

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

ทุกอย่างในแอปพร้อมตรวจสอบการซื้อแล้ว

ตั้งค่าบริการแบ็กเอนด์

ต่อไป ให้ตั้งค่าฟังก์ชันระบบคลาวด์เพื่อยืนยันการซื้อบนแบ็กเอนด์

สร้างเครื่องจัดการการซื้อ

เนื่องจากขั้นตอนการยืนยันสำหรับร้านค้าทั้ง 2 แห่งมีความคล้ายคลึงกัน โปรดตั้งค่าคลาส PurchaseHandler แบบนามธรรมที่มีการติดตั้งใช้งานแยกกันสำหรับแต่ละร้านค้า

be50c207c5a2a519.png

เริ่มต้นด้วยการเพิ่มไฟล์ purchase_handler.dart ไปยังโฟลเดอร์ lib/ ซึ่งคุณจะกําหนดคลาส PurchaseHandler แบบนามธรรมที่มีวิธีการแบบนามธรรม 2 แบบในการยืนยันการซื้อที่แตกต่างกัน 2 ประเภท ได้แก่ การสมัครใช้บริการและการไม่สมัครใช้บริการ

lib/purchase_handler.dart

import 'products.dart';

/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {

  /// Verify if non-subscription purchase (aka consumable) is valid
  /// and update the database
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });

  /// Verify if subscription purchase (aka non-consumable) is valid
  /// and update the database
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });
}

ดังที่คุณเห็น แต่ละเมธอดต้องมีพารามิเตอร์ 3 ตัว ดังนี้

  • userId: รหัสของผู้ใช้ที่เข้าสู่ระบบเพื่อให้คุณผูกการซื้อกับผู้ใช้ได้
  • productData: ข้อมูลเกี่ยวกับผลิตภัณฑ์ คุณจะกำหนดสิ่งนี้ในอีก 1 นาที
  • token: โทเค็นที่ร้านค้าให้ผู้ใช้

นอกจากนี้ เพื่อให้เครื่องจัดการการซื้อเหล่านี้ใช้งานง่ายยิ่งขึ้น ให้เพิ่มเมธอด verifyPurchase() ที่ใช้ได้ทั้งสำหรับการสมัครใช้บริการและการที่ไม่ใช่การสมัครใช้บริการ ดังนี้

lib/purchase_handler.dart

  /// Verify if purchase is valid and update the database
  Future<bool> verifyPurchase({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    switch (productData.type) {
      case ProductType.subscription:
        return handleSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
      case ProductType.nonSubscription:
        return handleNonSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
    }
  }

ตอนนี้คุณสามารถเรียกใช้ verifyPurchase สำหรับทั้ง 2 กรณีได้เลย แต่ยังคงมีการติดตั้งใช้งานที่แยกกัน

คลาส ProductData มีข้อมูลพื้นฐานเกี่ยวกับผลิตภัณฑ์ต่างๆ ที่ซื้อได้ ซึ่งรวมถึงรหัสผลิตภัณฑ์ (บางครั้งเรียกว่า SKU) และ ProductType

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

  const ProductData(this.productId, this.type);
}

ProductType อาจเป็นการสมัครใช้บริการหรือไม่สมัครใช้บริการก็ได้

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

สุดท้าย รายการผลิตภัณฑ์จะได้รับการกำหนดเป็นแผนที่ในไฟล์เดียวกัน

lib/products.dart

const productDataMap = {
  'dash_consumable_2k': ProductData(
    'dash_consumable_2k',
    ProductType.nonSubscription,
  ),
  'dash_upgrade_3d': ProductData(
    'dash_upgrade_3d',
    ProductType.nonSubscription,
  ),
  'dash_subscription_doubler': ProductData(
    'dash_subscription_doubler',
    ProductType.subscription,
  ),
};

ถัดไป ให้กำหนดการใช้งานตัวยึดตำแหน่งบางอย่างสำหรับ Google Play Store และ Apple App Store เริ่มต้นด้วย Google Play

สร้าง lib/google_play_purchase_handler.dart และเพิ่มชั้นเรียนที่ขยาย PurchaseHandler ที่คุณเพิ่งเขียน:

lib/google_play_purchase_handler.dart

import 'dart:async';

import 'package:googleapis/androidpublisher/v3.dart' as ap;

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
  );

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }
}

ในขณะนี้ จะแสดง true สำหรับเมธอดของตัวแฮนเดิล แล้วระบบจะนำคุณไปที่ลิงก์นั้นในภายหลัง

คุณอาจสังเกตเห็นแล้วว่าตัวสร้างใช้อินสแตนซ์ของ IapRepository เครื่องจัดการการซื้อจะใช้อินสแตนซ์นี้เพื่อเก็บข้อมูลเกี่ยวกับการซื้อใน Firestore ในภายหลัง หากต้องการสื่อสารกับ Google Play ให้ใช้AndroidPublisherApiที่ให้ไว้

ถัดไป ให้ทำแบบเดียวกันนี้กับเครื่องจัดการ App Store สร้าง lib/app_store_purchase_handler.dart และเพิ่มชั้นเรียนที่ขยาย PurchaseHandler อีกครั้ง:

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

  @override
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return true;
  }
}

เยี่ยม! ตอนนี้คุณมีเครื่องจัดการการซื้อ 2 รายการแล้ว ถัดไป ให้สร้างปลายทาง API การยืนยันการซื้อ

ใช้เครื่องจัดการการซื้อ

เปิด bin/server.dart และสร้างปลายทาง API โดยใช้ shelf_route:

bin/server.dart

Future<void> main() async {
  final router = Router();

  final purchaseHandlers = await _createPurchaseHandlers();

  router.post('/verifypurchase', (Request request) async {
    final dynamic payload = json.decode(await request.readAsString());

    final (:userId, :source, :productData, :token) = getPurchaseData(payload);

    final result = await purchaseHandlers[source]!.verifyPurchase(
      userId: userId,
      productData: productData,
      token: token,
    );

    if (result) {
      return Response.ok('all good!');
    } else {
      return Response.internalServerError();
    }
  });

  await serveHandler(router);
}

({
  String userId,
  String source,
  ProductData productData,
  String token,
}) getPurchaseData(dynamic payload) {
  if (payload
      case {
        'userId': String userId,
        'source': String source,
        'productId': String productId,
        'verificationData': String token,
      }) {
    return (
      userId: userId,
      source: source,
      productData: productDataMap[productId]!,
      token: token,
    );
  } else {
    throw const FormatException('Unexpected JSON');
  }
}

โค้ดด้านบนกำลังทำสิ่งต่อไปนี้

  1. กำหนดปลายทาง POST ที่จะถูกเรียกจากแอปที่คุณสร้างขึ้นก่อนหน้านี้
  2. ถอดรหัสเพย์โหลด JSON และแตกข้อมูลต่อไปนี้
  3. userId: รหัสผู้ใช้เข้าสู่ระบบอยู่ในขณะนี้
  4. source: ร้านค้ามือสอง ซึ่งอาจเป็น app_store หรือ google_play
  5. productData: ได้รับจากproductDataMap ที่คุณสร้างไว้ก่อนหน้านี้
  6. token: มีข้อมูลการยืนยันเพื่อส่งไปยังร้านค้า
  7. เรียกใช้เมธอด verifyPurchase สำหรับ GooglePlayPurchaseHandler หรือ AppStorePurchaseHandler ทั้งนี้ขึ้นอยู่กับแหล่งที่มา
  8. หากการยืนยันสำเร็จ เมธอดจะส่ง Response.ok คืนให้ลูกค้า
  9. หากการยืนยันไม่สำเร็จ เมธอดจะส่ง Response.internalServerError คืนให้กับไคลเอ็นต์

หลังจากสร้างปลายทาง API แล้ว คุณต้องกำหนดค่าเครื่องจัดการการซื้อ 2 รายการ ซึ่งคุณจะต้องโหลดคีย์บัญชีบริการที่ได้มาจากขั้นตอนก่อนหน้าและกำหนดค่าการเข้าถึงบริการต่างๆ ซึ่งรวมถึง Android Publisher API และ Firebase Firestore API จากนั้นสร้างเครื่องจัดการการซื้อ 2 รายการที่มีการอ้างอิงที่แตกต่างกันดังนี้

bin/server.dart

Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
  // Configure Android Publisher API access
  final serviceAccountGooglePlay =
      File('assets/service-account-google-play.json').readAsStringSync();
  final clientCredentialsGooglePlay =
      auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
  final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
  ]);
  final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);

  // Configure Firestore API access
  final serviceAccountFirebase =
      File('assets/service-account-firebase.json').readAsStringSync();
  final clientCredentialsFirebase =
      auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
  final clientFirebase =
      await auth.clientViaServiceAccount(clientCredentialsFirebase, [
    fs.FirestoreApi.cloudPlatformScope,
  ]);
  final firestoreApi = fs.FirestoreApi(clientFirebase);
  final dynamic json = jsonDecode(serviceAccountFirebase);
  final projectId = json['project_id'] as String;
  final iapRepository = IapRepository(firestoreApi, projectId);

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };
}

ยืนยันการซื้อใน Android: ใช้ตัวควบคุมการซื้อ

ถัดไป ให้ติดตั้งใช้งานเครื่องจัดการการซื้อของ Google Play ต่อ

Google มีแพ็กเกจ Dart สำหรับโต้ตอบกับ API ที่คุณต้องยืนยันการซื้ออยู่แล้ว คุณได้เริ่มต้นโปรแกรมเหล่านี้ในไฟล์ server.dart และตอนนี้ก็นำไปใช้ในชั้นเรียน GooglePlayPurchaseHandler แล้ว

ติดตั้งใช้งานตัวแฮนเดิลสำหรับการซื้อที่ไม่ใช่ประเภทการสมัครใช้บริการ

lib/google_play_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.products.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Purchases response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = response.orderId!;

      final purchaseData = NonSubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.purchaseTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _nonSubscriptionStatusFrom(response.purchaseState),
        userId: userId,
        iapSource: IAPSource.googleplay,
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle NonSubscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle NonSubscription: $e\n');
    }
    return false;
  }

โดยคุณจะอัปเดตเครื่องจัดการการซื้อการสมัครใช้บริการได้ด้วยวิธีที่คล้ายกัน ดังนี้

lib/google_play_purchase_handler.dart

  /// Handle subscription purchases.
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.subscriptions.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Subscription response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = extractOrderId(response.orderId!);

      final purchaseData = SubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.startTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _subscriptionStatusFrom(response.paymentState),
        userId: userId,
        iapSource: IAPSource.googleplay,
        expiryDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.expiryTimeMillis ?? '0'),
        ),
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

เพิ่มวิธีการต่อไปนี้เพื่อช่วยแยกวิเคราะห์รหัสคำสั่งซื้อ รวมถึงวิธีแยกวิเคราะห์สถานะการซื้อ 2 วิธี

lib/google_play_purchase_handler.dart

/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
  final orderIdSplit = orderId.split('..');
  if (orderIdSplit.isNotEmpty) {
    orderId = orderIdSplit[0];
  }
  return orderId;
}

NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
  return switch (state) {
    0 => NonSubscriptionStatus.completed,
    2 => NonSubscriptionStatus.pending,
    _ => NonSubscriptionStatus.cancelled,
  };
}

SubscriptionStatus _subscriptionStatusFrom(int? state) {
  return switch (state) {
    // Payment pending
    0 => SubscriptionStatus.pending,
    // Payment received
    1 => SubscriptionStatus.active,
    // Free trial
    2 => SubscriptionStatus.active,
    // Pending deferred upgrade/downgrade
    3 => SubscriptionStatus.pending,
    // Expired or cancelled
    _ => SubscriptionStatus.expired,
  };
}

ตอนนี้การซื้อใน Google Play ควรได้รับการยืนยันและจัดเก็บไว้ในฐานข้อมูลแล้ว

ต่อไปให้ไปที่การซื้อใน App Store สำหรับ iOS

ยืนยันการซื้อใน iOS: ใช้เครื่องจัดการการซื้อ

สำหรับการยืนยันการซื้อด้วย App Store จะมีแพ็กเกจ Dart ของบุคคลที่สามชื่อ app_store_server_sdk อยู่ ซึ่งทำให้กระบวนการนี้ง่ายขึ้น

เริ่มต้นด้วยการสร้างอินสแตนซ์ ITunesApi ใช้การกำหนดค่าแซนด์บ็อกซ์และเปิดใช้การบันทึกเพื่อช่วยให้แก้ไขข้อบกพร่องข้อผิดพลาดได้ง่ายขึ้น

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(
      ITunesEnvironment.sandbox(),
      loggingEnabled: true,
    ),
  );

ซึ่งตอนนี้ App Store ต่างจาก Google Play API ตรงที่ใช้ปลายทาง API เดียวกันสำหรับการสมัครใช้บริการและไม่ได้สมัครใช้บริการ ซึ่งหมายความว่าคุณสามารถใช้ตรรกะเดียวกันสำหรับตัวจัดการทั้ง 2 ตัวได้ ผสานรวมพร็อพเพอร์ตี้เหล่านั้นเข้าด้วยกันเพื่อให้เรียกการติดตั้งใช้งานเดียวกัน นั่นคือ

lib/app_store_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
   //..
  }

ตอนนี้ ให้ใช้ handleValidation:

lib/app_store_purchase_handler.dart

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
    print('AppStorePurchaseHandler.handleValidation');
    final response = await _iTunesAPI.verifyReceipt(
      password: appStoreSharedSecret,
      receiptData: token,
    );
    print('response: $response');
    if (response.status == 0) {
      print('Successfully verified purchase');
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              status: NonSubscriptionStatus.completed,
            ));
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0')),
              status: SubscriptionStatus.active,
            ));
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

การซื้อใน App Store ควรได้รับการยืนยันและจัดเก็บไว้ในฐานข้อมูลแล้ว

เรียกใช้แบ็กเอนด์

ณ จุดนี้ คุณสามารถเรียกใช้ dart bin/server.dart เพื่อให้บริการปลายทาง /verifypurchase

$ dart bin/server.dart 
Serving at http://0.0.0.0:8080

11. ติดตามการซื้อ

วิธีที่แนะนำในการติดตามผู้ใช้ของคุณ การซื้อจะอยู่ในบริการแบ็กเอนด์ ทั้งนี้เนื่องจากแบ็กเอนด์ของคุณสามารถตอบสนองต่อเหตุการณ์จาก Store ได้ จึงมีแนวโน้มที่จะรับข้อมูลที่ล้าสมัยน้อยลงเนื่องจากการแคช รวมถึงมีความเสี่ยงที่จะถูกดัดแปลงน้อยกว่าด้วย

ก่อนอื่นให้ตั้งค่าการประมวลผลเหตุการณ์ของร้านค้าบนแบ็กเอนด์ด้วยแบ็กเอนด์ของ DART ที่คุณกำลังสร้าง

ประมวลผลเหตุการณ์ของร้านค้าในแบ็กเอนด์

ร้านค้าสามารถแจ้งแบ็กเอนด์เกี่ยวกับเหตุการณ์การเรียกเก็บเงินที่เกิดขึ้นได้ เช่น เมื่อต่ออายุการสมัครใช้บริการ คุณสามารถประมวลผลเหตุการณ์เหล่านี้ในแบ็กเอนด์เพื่อให้การซื้อในฐานข้อมูลเป็นปัจจุบันอยู่เสมอ ในส่วนนี้ ให้ตั้งค่านี้สำหรับทั้ง Google Play Store และ Apple App Store

ประมวลผลกิจกรรมการเรียกเก็บเงินของ Google Play

Google Play ให้บริการกิจกรรมการเรียกเก็บเงินผ่านสิ่งที่เรียกว่าหัวข้อ Pub/Sub ของระบบคลาวด์ โดยพื้นฐานแล้วคือคิวข้อความที่สามารถใช้เผยแพร่และเรียกดูข้อความได้

เนื่องจากนี่เป็นฟังก์ชันการทำงานเฉพาะสำหรับ Google Play คุณจึงรวมฟังก์ชันนี้ไว้ในGooglePlayPurchaseHandler

เริ่มต้นด้วยการเปิด lib/google_play_purchase_handler.dart และเพิ่มการนำเข้า PubsubApi ดังนี้

lib/google_play_purchase_handler.dart

import 'package:googleapis/pubsub/v1.dart' as pubsub;

จากนั้นส่ง PubsubApi ไปยัง GooglePlayPurchaseHandler และแก้ไขตัวสร้างคลาสเพื่อสร้าง Timer ดังนี้

lib/google_play_purchase_handler.dart

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;
  final pubsub.PubsubApi pubsubApi; // new

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
    this.pubsubApi, // new
  ) {
    // Poll messages from Pub/Sub every 10 seconds
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullMessageFromPubSub();
    });
  }

มีการกำหนดค่า Timer ให้เรียกใช้เมธอด _pullMessageFromSubSub ทุก 10 วินาที คุณสามารถปรับระยะเวลาได้ตามต้องการ

จากนั้นสร้าง _pullMessageFromSubSub

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(
      maxMessages: 1000,
    );
    final topicName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

  Future<void> _processMessage(String data64, String? ackId) async {
    final dataRaw = utf8.decode(base64Decode(data64));
    print('Received data: $dataRaw');
    final dynamic data = jsonDecode(dataRaw);
    if (data['testNotification'] != null) {
      print('Skip test messages');
      if (ackId != null) {
        await _ackMessage(ackId);
      }
      return;
    }
    final dynamic subscriptionNotification = data['subscriptionNotification'];
    final dynamic oneTimeProductNotification =
        data['oneTimeProductNotification'];
    if (subscriptionNotification != null) {
      print('Processing Subscription');
      final subscriptionId =
          subscriptionNotification['subscriptionId'] as String;
      final purchaseToken = subscriptionNotification['purchaseToken'] as String;
      final productData = productDataMap[subscriptionId]!;
      final result = await handleSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else if (oneTimeProductNotification != null) {
      print('Processing NonSubscription');
      final sku = oneTimeProductNotification['sku'] as String;
      final purchaseToken =
          oneTimeProductNotification['purchaseToken'] as String;
      final productData = productDataMap[sku]!;
      final result = await handleNonSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else {
      print('invalid data');
    }
  }

  /// ACK Messages from Pub/Sub
  Future<void> _ackMessage(String id) async {
    print('ACK Message');
    final request = pubsub.AcknowledgeRequest(
      ackIds: [id],
    );
    final subscriptionName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

รหัสที่คุณเพิ่งเพิ่มจะสื่อสารกับหัวข้อ Pub/Sub จาก Google Cloud ทุกๆ 10 วินาทีและขอข้อความใหม่ จากนั้นจึงประมวลผลแต่ละข้อความในเมธอด _processMessage

วิธีการนี้จะถอดรหัสข้อความขาเข้าและรับข้อมูลที่อัปเดตเกี่ยวกับการซื้อแต่ละครั้ง ทั้งการสมัครรับข้อมูลและการที่ไม่ใช่การสมัครรับข้อมูล โดยจะเรียกใช้ handleSubscription หรือ handleNonSubscription ที่มีอยู่หากจำเป็น

คุณต้องรับทราบแต่ละข้อความโดยใช้เมธอด _askMessage

จากนั้นเพิ่มทรัพยากร Dependency ที่จำเป็นลงในไฟล์ server.dart เพิ่ม PubsubApi.cloudPlatformScope ในการกำหนดค่าข้อมูลเข้าสู่ระบบ ดังนี้

bin/server.dart

 final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
    pubsub.PubsubApi.cloudPlatformScope, // new
  ]);

จากนั้นสร้างอินสแตนซ์ PubsubApi ด้วยคำสั่งต่อไปนี้

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

และสุดท้ายให้ส่งต่อให้กับตัวสร้าง GooglePlayPurchaseHandler:

bin/server.dart

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi, // new
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };

การตั้งค่า Google Play

คุณได้เขียนโค้ดเพื่อใช้กิจกรรมการเรียกเก็บเงินจากหัวข้อ pub/sub แต่ยังไม่ได้สร้างหัวข้อ pub/sub และคุณไม่ได้เผยแพร่กิจกรรมการเรียกเก็บเงินใดๆ ได้เวลาตั้งค่านี้แล้ว

ขั้นแรก ให้สร้างหัวข้อ pub/sub โดยทำดังนี้

  1. ไปที่หน้า Cloud Pub/Sub บน Google Cloud Console
  2. ตรวจสอบว่าคุณอยู่ในโปรเจ็กต์ Firebase แล้วคลิก + สร้างหัวข้อ d5ebf6897a0a8bf5.png
  3. ตั้งชื่อหัวข้อใหม่ให้เหมือนกับค่าที่ตั้งไว้สำหรับ GOOGLE_PLAY_PUBSUB_BILLING_TOPIC ใน constants.ts ในกรณีนี้ ให้ตั้งชื่อว่า play_billing หากเลือกรายการอื่น อย่าลืมอัปเดต constants.ts สร้างหัวข้อ 20d690fc543c4212.png
  4. ในรายการหัวข้อ Pub/Sub ให้คลิกจุดแนวตั้ง 3 จุดสำหรับหัวข้อที่คุณเพิ่งสร้าง แล้วคลิกดูสิทธิ์ ea03308190609fb.png
  5. ในแถบด้านข้างทางขวา ให้เลือกเพิ่มผู้ใช้หลัก
  6. ตรงนี้ ให้เพิ่ม google-play-developer-notifications@system.gserviceaccount.com และมอบบทบาทผู้เผยแพร่ Pub/Sub 55631ec0549215bc.png
  7. บันทึกการเปลี่ยนแปลงสิทธิ์
  8. คัดลอกชื่อหัวข้อของหัวข้อที่คุณเพิ่งสร้าง
  9. เปิด Play Console อีกครั้ง แล้วเลือกแอปจากรายการแอปทั้งหมด
  10. เลื่อนลงและไปที่สร้างรายได้ > การตั้งค่าการสร้างรายได้
  11. กรอกหัวข้อทั้งหมดและบันทึกการเปลี่ยนแปลง 7e5e875dc6ce5d54.png

ตอนนี้เหตุการณ์ทั้งหมดใน Google Play Billing จะได้รับการเผยแพร่เกี่ยวกับหัวข้อนี้

ประมวลผลเหตุการณ์การเรียกเก็บเงินของ App Store

ให้ทำแบบเดียวกันนี้สำหรับกิจกรรมการเรียกเก็บเงินของ App Store วิธีที่มีประสิทธิภาพในการจัดการการอัปเดตสำหรับการซื้อ App Store มีอยู่ 2 วิธีด้วยกัน หนึ่งในนั้นคือการใช้เว็บฮุคที่คุณให้กับ Apple ซึ่งทำหน้าที่สื่อสารกับเซิร์ฟเวอร์ของคุณ วิธีที่ 2 ซึ่งคุณจะพบใน Codelab นี้ก็คือการเชื่อมต่อกับ App Store Server API และรับข้อมูลการสมัครใช้บริการด้วยตนเอง

เหตุผลที่ Codelab นี้มุ่งเน้นโซลูชันที่ 2 ก็เพราะว่าคุณจะต้องเปิดเผยเซิร์ฟเวอร์ต่ออินเทอร์เน็ตเพื่อใช้งานเว็บฮุค

ในสภาพแวดล้อมการใช้งานจริง คุณควรมีทั้ง 2 อย่าง เว็บฮุคสำหรับรับเหตุการณ์จาก App Store และ Server API ในกรณีที่คุณไม่มีกิจกรรมหรือต้องการตรวจสอบสถานะการสมัครใช้บริการอีกครั้ง

เริ่มต้นด้วยการเปิด lib/app_store_purchase_handler.dart และเพิ่มทรัพยากร Dependency ของ AppStoreServerAPI:

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

AppStorePurchaseHandler(
  this.iapRepository,
  this.appStoreServerAPI, // new
)

แก้ไขเครื่องมือสร้างเพื่อเพิ่มตัวจับเวลาที่จะเรียกเมธอด _pullStatus ตัวจับเวลานี้จะเรียกใช้เมธอด _pullStatus ทุก 10 วินาที คุณปรับระยะเวลาของตัวจับเวลานี้ได้ตามความต้องการ

lib/app_store_purchase_handler.dart

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,
  ) {
    // Poll Subscription status every 10 seconds.
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullStatus();
    });
  }

จากนั้นสร้างเมธอด _pullStatus ดังนี้

lib/app_store_purchase_handler.dart

  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where((element) =>
        element.type == ProductType.subscription &&
        element.iapSource == IAPSource.appstore);
    for (final purchase in appStoreSubscriptions) {
      final status =
          await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
      // Obtain all subscriptions for the order id.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
              transaction.transactionInfo.expiresDate ?? 0);
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(SubscriptionPurchase(
            userId: null,
            productId: transaction.transactionInfo.productId,
            iapSource: IAPSource.appstore,
            orderId: transaction.originalTransactionId,
            purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate),
            type: ProductType.subscription,
            expiryDate: expirationDate,
            status: isExpired
                ? SubscriptionStatus.expired
                : SubscriptionStatus.active,
          ));
        }
      }
    }
  }

วิธีการนี้มีการทำงานดังนี้

  1. รับรายการการสมัครใช้บริการที่ใช้งานอยู่จาก Firestore โดยใช้ IapRepository
  2. และจะขอสถานะการสมัครใช้บริการ App Store Server API สำหรับคำสั่งซื้อแต่ละรายการ
  3. รับธุรกรรมล่าสุดสำหรับการซื้อการสมัครใช้บริการนั้นๆ
  4. ตรวจสอบวันที่หมดอายุ
  5. อัปเดตสถานะการสมัครใช้บริการใน Firestore หากหมดอายุ ระบบจะทำเครื่องหมายให้ทราบ

สุดท้าย เพิ่มโค้ดที่จำเป็นทั้งหมดเพื่อกำหนดค่าการเข้าถึง API ของเซิร์ฟเวอร์ App Store ดังนี้

bin/server.dart

  // add from here
  final subscriptionKeyAppStore =
      File('assets/SubscriptionKey.p8').readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here


  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
      appStoreServerAPI, // new
    ),
  };

การตั้งค่า App Store

ต่อไป ให้ตั้งค่า App Store ดังนี้

  1. เข้าสู่ระบบ App Store Connect แล้วเลือกผู้ใช้และการเข้าถึง
  2. ไปที่ ประเภทคีย์ > การซื้อในแอป
  3. แตะ "เครื่องหมายบวก" เพื่อเพิ่มใหม่
  4. ตั้งชื่อ เช่น "คีย์ Codelab"
  5. ดาวน์โหลดไฟล์ p8 ที่มีคีย์ดังกล่าว
  6. คัดลอกไปยังโฟลเดอร์เนื้อหาโดยใช้ชื่อ SubscriptionKey.p8
  7. คัดลอกรหัสคีย์จากคีย์ที่สร้างใหม่และตั้งค่าเป็นค่าคงที่ appStoreKeyId ในไฟล์ lib/constants.dart
  8. คัดลอกรหัสผู้ออกคีย์ไปที่ด้านบนของรายการคีย์แล้วตั้งค่าเป็นค่าคงที่ appStoreIssuerId ในไฟล์ lib/constants.dart

9540ea9ada3da151.png

ติดตามการซื้อในอุปกรณ์

วิธีที่ปลอดภัยที่สุดในการติดตามการซื้อของคุณคือฝั่งเซิร์ฟเวอร์เนื่องจากไคลเอ็นต์ยากที่จะรักษาความปลอดภัย แต่คุณต้องมีวิธีที่จะดึงข้อมูลกลับไปยังไคลเอ็นต์เพื่อให้แอปดำเนินการกับข้อมูลสถานะการสมัครรับข้อมูลได้ การจัดเก็บการซื้อใน Firestore จะช่วยให้คุณซิงค์ข้อมูลไปยังไคลเอ็นต์และอัปเดตโดยอัตโนมัติได้อย่างง่ายดาย

คุณรวม IAPRepo ไว้ในแอปแล้ว ซึ่งเป็นที่เก็บ Firestore ที่มีข้อมูลการซื้อของผู้ใช้ทั้งหมดใน List<PastPurchase> purchases ที่เก็บยังมี hasActiveSubscription, ซึ่งเป็นจริงเมื่อมีการซื้อด้วย productId storeKeySubscription ที่มีสถานะเป็นยังไม่หมดอายุ เมื่อผู้ใช้ไม่ได้ลงชื่อเข้าสู่ระบบ รายการนี้จะว่างเปล่า

lib/repo/iap_repo.dart

  void updatePurchases() {
    _purchaseSubscription?.cancel();
    var user = _user;
    if (user == null) {
      purchases = [];
      hasActiveSubscription = false;
      hasUpgrade = false;
      return;
    }
    var purchaseStream = _firestore
        .collection('purchases')
        .where('userId', isEqualTo: user.uid)
        .snapshots();
    _purchaseSubscription = purchaseStream.listen((snapshot) {
      purchases = snapshot.docs.map((DocumentSnapshot document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

      hasActiveSubscription = purchases.any((element) =>
          element.productId == storeKeySubscription &&
          element.status != Status.expired);

      hasUpgrade = purchases.any(
        (element) => element.productId == storeKeyUpgrade,
      );

      notifyListeners();
    });
  }

ตรรกะการซื้อทั้งหมดอยู่ในคลาส DashPurchases และเป็นจุดที่ควรใช้หรือนำการสมัครใช้บริการออก ดังนั้น ให้เพิ่ม iapRepo เป็นพร็อพเพอร์ตี้ในชั้นเรียน และกำหนด iapRepo ในตัวสร้าง ถัดไป ให้เพิ่ม Listener ในตัวสร้างโดยตรง และนำ Listener ออกในเมธอด dispose() ในช่วงแรก Listener จะเป็นแค่ฟังก์ชันว่างได้ เนื่องจาก IAPRepo เป็น ChangeNotifier และคุณเรียกใช้ notifyListeners() ทุกครั้งที่การซื้อใน Firestore มีการเปลี่ยนแปลง เมธอด purchasesUpdate() จะถูกเรียกเสมอเมื่อผลิตภัณฑ์ที่ซื้อมีการเปลี่ยนแปลง

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

  DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
    final purchaseUpdated =
        iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    iapRepo.addListener(purchasesUpdate);
    loadPurchases();
  }

  @override
  void dispose() {
    iapRepo.removeListener(purchasesUpdate);
    _subscription.cancel();
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

ถัดไป ให้ระบุ IAPRepo ให้กับเครื่องมือสร้างใน main.dart. คุณจะรับที่เก็บได้โดยใช้ context.read เนื่องจากมีการสร้างไว้ใน Provider แล้ว

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
            context.read<IAPRepo>(),
          ),
          lazy: false,
        ),

ถัดไป ให้เขียนโค้ดสำหรับฟังก์ชัน purchaseUpdate() ใน dash_counter.dart, เมธอด applyPaidMultiplier และ removePaidMultiplier จะตั้งค่าตัวคูณเป็น 10 หรือ 1 ตามลำดับ คุณจึงไม่จำเป็นต้องตรวจสอบว่าได้ใช้การสมัครใช้บริการแล้วหรือไม่ เมื่อสถานะการสมัครใช้บริการมีการเปลี่ยนแปลง คุณยังอัปเดตสถานะของผลิตภัณฑ์ที่ซื้อได้เพื่อแสดงในหน้าการซื้อว่าผลิตภัณฑ์ดังกล่าวมีการใช้งานอยู่ด้วย ตั้งค่าพร็อพเพอร์ตี้ _beautifiedDashUpgrade ตามการซื้อการอัปเกรด

lib/logic/dash_purchases.dart

void purchasesUpdate() {
    var subscriptions = <PurchasableProduct>[];
    var upgrades = <PurchasableProduct>[];
    // Get a list of purchasable products for the subscription and upgrade.
    // This should be 1 per type.
    if (products.isNotEmpty) {
      subscriptions = products
          .where((element) => element.productDetails.id == storeKeySubscription)
          .toList();
      upgrades = products
          .where((element) => element.productDetails.id == storeKeyUpgrade)
          .toList();
    }

    // Set the subscription in the counter logic and show/hide purchased on the
    // purchases page.
    if (iapRepo.hasActiveSubscription) {
      counter.applyPaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchased);
      }
    } else {
      counter.removePaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchasable);
      }
    }

    // Set the Dash beautifier and show/hide purchased on
    // the purchases page.
    if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
      _beautifiedDashUpgrade = iapRepo.hasUpgrade;
      for (var element in upgrades) {
        _updateStatus(
          element,
          _beautifiedDashUpgrade
              ? ProductStatus.purchased
              : ProductStatus.purchasable);
      }
      notifyListeners();
    }
  }

  void _updateStatus(PurchasableProduct product, ProductStatus status) {
    if (product.status != ProductStatus.purchased) {
      product.status = ProductStatus.purchased;
      notifyListeners();
    }
  }

ตอนนี้คุณก็ทำให้สถานะการสมัครใช้บริการและการอัปเกรดเป็นข้อมูลล่าสุดเสมอในบริการแบ็กเอนด์และซิงค์กับแอปแล้ว แอปจะทำงานตามนั้นและใช้ฟีเจอร์การสมัครใช้บริการและการอัปเกรดกับเกม Dasher ของคุณ

12. เสร็จเรียบร้อย

ขอแสดงความยินดี! คุณทำ Codelab เสร็จสมบูรณ์แล้ว คุณดูโค้ดที่เสร็จสมบูรณ์ของ Codelab ได้ในandroid_studio_โฟลเดอร์.pngโฟลเดอร์ที่เสร็จสมบูรณ์

ลองดูข้อมูลเพิ่มเติมที่ Flutter Codelab