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

1. บทนำ

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

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

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

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

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

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

บริการแบ็กเอนด์ (มีให้ใช้งานด้วย) จะทำงานเป็นแอป 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 นี้อยู่ใน 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 Code ก็เป็นอีกตัวเลือกที่ยอดเยี่ยม เมื่อใช้เครื่องมือแก้ไขใดก็ตาม โปรดตรวจสอบว่าได้ติดตั้งปลั๊กอิน 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 และ Products จะอยู่ใต้โปรเจ็กต์ Runner คลิกสองครั้งที่ Runner เพื่อแก้ไขการตั้งค่าโปรเจ็กต์ แล้วคลิกการรับรองและความสามารถ ป้อนตัวระบุกลุ่มที่เพิ่งเลือกไว้ในช่องทีมเพื่อตั้งค่าทีม

812f919d965c649a.jpeg

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

5c4733ac560ae8c2.png

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

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

เพิ่มข้อกำหนดใน pubspec

เพิ่ม in_app_purchase ลงใน pubspec โดยเพิ่ม in_app_purchase ลงใน Dependencies ใน pubspec

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface

เปิด pubspec.yaml และตรวจสอบว่าคุณมี in_app_purchase แสดงเป็นรายการใต้ dependencies และ in_app_purchase_platform_interface ใต้ dev_dependencies

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^5.5.1
  cupertino_icons: ^1.0.8
  firebase_auth: ^5.3.4
  firebase_core: ^3.8.1
  google_sign_in: ^6.2.2
  http: ^1.2.2
  intl: ^0.20.1
  provider: ^6.1.2
  in_app_purchase: ^3.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0
  in_app_purchase_platform_interface: ^1.4.0

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

4. ตั้งค่า App Store

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

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

11db9fca823e7608.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 > บัญชีแซนด์บ็อกซ์

d99e0b89673867cd.jpeg e1621bcaeb33d3c5.jpeg

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

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

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

d156b2f5bac43ca8.png

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

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

  1. ตั้งค่า dash_consumable_2k เป็นไอเทมที่บริโภคได้

ใช้ dash_consumable_2k เป็นรหัสผลิตภัณฑ์ ชื่ออ้างอิงจะใช้ใน App Store Connect เท่านั้น เพียงตั้งค่าเป็น 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 คลิกบันทึก

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 Keep the

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 ให้ไปที่รุ่น > การทดสอบ > การทดสอบแบบปิด แล้วสร้างรุ่นการทดสอบแบบปิดใหม่

สําหรับโค้ดแล็บนี้ คุณจะใช้ Google เป็นผู้ลงนามในแอป ดังนั้นให้กดต่อไปในส่วน Play App Signing เพื่อเลือกใช้

ba98446d9c5c40e0.png

ถัดไป ให้อัปโหลด App Bundle app-release.aab ที่สร้างขึ้นโดยคําสั่ง build

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

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

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

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

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

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

a0d0394e85128f84.png

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

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

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

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

a1a0f9d3e55ea8da.png

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

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

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

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

ก่อนอื่นให้เพิ่มไอเทมที่บริโภคได้และบริโภคไม่ได้

  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

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

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

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

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

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

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

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

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

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

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

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

7babb48832fbef29.png

เนื่องจากคุณจะใช้ฐานข้อมูล 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 configure ให้เลือกโปรเจ็กต์ที่คุณเพิ่งสร้างในขั้นตอนก่อนหน้า

$ 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 ในโหมดแก้ไขข้อบกพร่อง คุณต้องระบุลายนิ้วมือแฮช SHA-1 ของใบรับรองแก้ไขข้อบกพร่อง

รับแฮชใบรับรองการรับรองสำหรับการแก้ไขข้อบกพร่อง

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

cd android
./gradlew :app:signingReport

คุณจะเห็นรายการคีย์การรับรองจำนวนมาก เนื่องจากคุณกําลังมองหาแฮชสําหรับใบรับรองการแก้ไขข้อบกพร่อง ให้มองหาใบรับรองที่มีการตั้งค่าพร็อพเพอร์ตี้ 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. ฟังข้อมูลอัปเดตเกี่ยวกับการซื้อ

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

ฟังการอัปเดตการซื้อ

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

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

lib/main.dart

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

นอกจากนี้ คุณจะต้องมีอินสแตนซ์ของ 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!;
  }
}

คุณต้องอัปเดตการทดสอบเล็กน้อยหากต้องการให้การทดสอบทำงานต่อไป

test/widget_test.dart

import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import

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

class TestIAPConnection implements InAppPurchase {         // Add from here
  @override
  Future<bool> buyConsumable(
      {required PurchaseParam purchaseParam, bool autoConsume = true}) {
    return Future.value(false);
  }

  @override
  Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
    return Future.value(false);
  }

  @override
  Future<void> completePurchase(PurchaseDetails purchase) {
    return Future.value();
  }

  @override
  Future<bool> isAvailable() {
    return Future.value(false);
  }

  @override
  Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
    return Future.value(ProductDetailsResponse(
      productDetails: [],
      notFoundIDs: [],
    ));
  }

  @override
  T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
    // TODO: implement getPlatformAddition
    throw UnimplementedError();
  }

  @override
  Stream<List<PurchaseDetails>> get purchaseStream =>
      Stream.value(<PurchaseDetails>[]);

  @override
  Future<void> restorePurchases({String? applicationUserName}) {
    // TODO: implement restorePurchases
    throw UnimplementedError();
  }

  @override
  Future<String> countryCode() {
    // TODO: implement countryCode
    throw UnimplementedError();
  }
}                                                          // To here.

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

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

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import

import '../main.dart';                                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.available;
  late StreamSubscription<List<PurchaseDetails>> _subscription;  // Add this line
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;            // And this line

  DashPurchases(this.counter);

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
}

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

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

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.notAvailable;         // Modify this line
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

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

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

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
                                                           // Add from here
  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

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

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

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

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

$ flutter test

00:01 +1: All tests passed!                                                                                   

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

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

ปรับ PurchasableProduct

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;
    }
  }

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

เมธอด 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);
    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(),
    );
  }
}

คุณควรเห็นผลิตภัณฑ์ที่พร้อมจำหน่ายใน Store ของ 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);
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
      default:
        throw ArgumentError.value(
            product.productDetails, '${product.id} is not a known product');
    }
  }

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

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();
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
      }
    }

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

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

ก่อนติดตามและยืนยันการซื้อ ให้ตั้งค่าแบ็กเอนด์ Dart เพื่อรองรับการดำเนินการดังกล่าว

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

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

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

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

โค้ดแบ็กเอนด์นี้สามารถทำงานในเครื่องได้ คุณจึงไม่ต้องทำให้ใช้งานได้ อย่างไรก็ตาม คุณต้องเชื่อมต่อจากอุปกรณ์สำหรับพัฒนาซอฟต์แวร์ (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 หรือฐานข้อมูลโดยทั่วไปไม่เกี่ยวข้องกับโค้ดแล็บนี้ โค้ดเริ่มต้นจึงมีฟังก์ชันสำหรับให้คุณสร้างหรืออัปเดตการซื้อใน Firestore รวมถึงคลาสทั้งหมดสำหรับการซื้อเหล่านั้น

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

หากต้องการเข้าถึง Firebase Firestore คุณต้องมีคีย์การเข้าถึงบัญชีบริการ สร้างคีย์โดยเปิดการตั้งค่าโปรเจ็กต์ 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 ในคอนโซลนักพัฒนาแอป แล้วคลิกรีเฟรชบัญชีบริการ คุณควรเห็นบัญชีที่สร้างใหม่ในรายการ 5895a7db8b4c7659.png
  10. คลิกให้สิทธิ์เข้าถึงสําหรับบัญชีบริการใหม่
  11. เลื่อนหน้าถัดไปลงไปที่บล็อกข้อมูลทางการเงิน เลือกทั้งดูข้อมูลทางการเงิน คำสั่งซื้อ และการตอบแบบสำรวจการยกเลิกและจัดการคำสั่งซื้อและการสมัครใช้บริการ 75b22d0201cf67e.png
  12. คลิกเชิญผู้ใช้ 70ea0b1288c62a59.png
  13. เมื่อตั้งค่าบัญชีแล้ว คุณก็แค่สร้างข้อมูลเข้าสู่ระบบ กลับไปที่คอนโซลระบบคลาวด์ แล้วค้นหาบัญชีบริการในรายการบัญชีบริการ คลิกจุดแนวตั้ง 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. คลิกรหัสลับที่แชร์เฉพาะแอปที่ด้านขวาบนของรายการ
  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 ร้านค้า แอปพลิเคชันของคุณจะได้รับโทเค็นเมื่อมีการซื้อ

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

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

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

ตั้งค่าฝั่ง Flutter

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

เนื่องจากคุณกำลังจะส่งการซื้อไปยังบริการแบ็กเอนด์ คุณจึงต้องตรวจสอบว่าผู้ใช้ได้รับการตรวจสอบสิทธิ์ขณะทำการซื้อ เราได้เพิ่มตรรกะการตรวจสอบสิทธิ์ส่วนใหญ่ไว้ในโปรเจ็กต์เริ่มต้นแล้ว คุณเพียงแค่ต้องตรวจสอบว่า PurchasePage แสดงปุ่มเข้าสู่ระบบเมื่อผู้ใช้ยังไม่ได้เข้าสู่ระบบ เพิ่มโค้ดต่อไปนี้ไว้ที่ตอนต้นของเมธอด build ของ 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({super.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 POST

ส่งร้านค้าที่เลือก (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 เพื่อให้ส่งรหัสผู้ใช้ไปยังฟังก์ชันยืนยันการซื้อได้

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) {
      return true;
    } else {
      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();
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
        }
      }
    }

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

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

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

ถัดไป ให้ตั้งค่า Cloud Function เพื่อยืนยันการซื้อในแบ็กเอนด์

สร้างตัวแฮนเดิลการซื้อ

เนื่องจากขั้นตอนการยืนยันของทั้ง 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: ข้อมูลเกี่ยวกับผลิตภัณฑ์ คุณจะต้องกําหนดค่านี้ในอีกสักครู่
  • 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.call);
}

({
  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

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,
  };
}

/// 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;
}

ตอนนี้การซื้อใน 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 ใช้ปลายทาง API เดียวกันสำหรับทั้งการสมัครใช้บริการและการไม่สมัครใช้บริการ ซึ่งแตกต่างจาก Google Play 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) {
      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. ติดตามการซื้อ

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

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

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

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

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

Google Play ระบุเหตุการณ์การเรียกเก็บเงินผ่านสิ่งที่เรียกว่าหัวข้อ Cloud 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

จากนั้นเพิ่มข้อกำหนดที่จำเป็นลงในไฟล์ 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
  2. ตรวจสอบว่าคุณอยู่ในโปรเจ็กต์ Firebase แล้วคลิก + สร้างหัวข้อ d5ebf6897a0a8bf5.png
  3. ตั้งชื่อหัวข้อใหม่ให้เหมือนกับค่าที่ตั้งไว้สำหรับ GOOGLE_PLAY_PUBSUB_BILLING_TOPIC ใน constants.ts ในกรณีนี้ ให้ตั้งชื่อเป็น play_billing หากเลือกอย่างอื่น โปรดอัปเดต constants.ts สร้างหัวข้อ 20d690fc543c4212.png
  4. ในรายการหัวข้อที่เผยแพร่/ติดตาม ให้คลิกจุดแนวตั้ง 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 ในหัวข้อนี้

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

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

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

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

เริ่มต้นด้วยการเปิด lib/app_store_purchase_handler.dart แล้วเพิ่ม 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 หากหมดอายุ ระบบจะทำเครื่องหมายเป็นเช่นนั้น

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

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 ในคอนสตรัคเตอร์ จากนั้นเพิ่มตัวรับฟังในคอนสตรคเตอร์โดยตรง และนําตัวรับฟังออกในเมธอด 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() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);
    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();
    }
  }

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

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

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

ดูข้อมูลเพิ่มเติมได้ใน Flutter Codelab อื่นๆ