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

1. บทนำ

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

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

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

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

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

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

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

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

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

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

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

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

สิ่งที่คุณต้องมี

  • Android Studio
  • Xcode (สำหรับการพัฒนา 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/app ใน IDE ที่คุณชื่นชอบ เราใช้ Android Studio สำหรับภาพหน้าจอ แต่ Visual Studio Code ก็เป็นตัวเลือกที่ดีเช่นกัน ไม่ว่าจะใช้โปรแกรมแก้ไขใด ให้ตรวจสอบว่าได้ติดตั้งปลั๊กอิน Dart และ Flutter เวอร์ชันล่าสุดแล้ว

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

ก่อนอื่น ให้ตั้งค่าตัวระบุชุดสำหรับ iOS โดยเปิดไฟล์ Runner.xcworkspace ในแอป Xcode

a9fbac80a31e28e0.png

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

812f919d965c649a.jpeg

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

e320a49ff2068ac2.png

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

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

เพิ่มการอ้างอิงใน pubspec

เพิ่ม in_app_purchase ลงใน pubspec โดยเพิ่ม in_app_purchase ลงในทรัพยากร Dependency ของโปรเจ็กต์

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  flutter_lints 5.0.0 (6.0.0 available)
+ in_app_purchase 3.2.3
+ in_app_purchase_android 0.4.0+3
+ in_app_purchase_platform_interface 1.4.0
+ in_app_purchase_storekit 0.4.4
+ json_annotation 4.9.0
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.16.0 (1.17.0 available)
  provider 6.1.5 (6.1.5+1 available)
  test_api 0.7.6 (0.7.7 available)
Changed 5 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

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

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

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

4. ตั้งค่า App Store

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

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

11db9fca823e7608.png

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

74c73197472c9aec.png

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

4a100bbb8cafdbbf.jpeg

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

สร้างตัวระบุใหม่ในพอร์ทัลนักพัฒนาแอปของ Apple ไปที่ developer.apple.com/account/resources/identifiers/list แล้วคลิกไอคอน "บวก" ข้างส่วนหัวตัวระบุ

55d7e592d9a3fc7b.png

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

13f125598b72ca77.png

เลือกแอป

41ac4c13404e2526.png

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

9d2c940ad80deeef.png

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

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

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

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

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

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

2ba0f599bcac9b36.png

ตอนนี้คุณตั้งค่าผู้ใช้ Sandbox ใน iPhone ได้โดยไปที่การตั้งค่า > นักพัฒนาแอป > บัญชี Apple Sandbox

74a545210b282ad8.png eaa67752f2350f74.png

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

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

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

a118161fac83815a.png

ไปที่การซื้อในแอป

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

  1. ตั้งค่า dash_consumable_2k เป็นไอเทมสิ้นเปลือง ใช้ dash_consumable_2k เป็นรหัสผลิตภัณฑ์ ชื่ออ้างอิงจะใช้ใน App Store Connect เท่านั้น เพียงตั้งค่าเป็น dash consumable 2k 1f8527fc03902099.png ตั้งค่าความพร้อมให้บริการ ผลิตภัณฑ์ต้องพร้อมให้บริการในประเทศของผู้ใช้แซนด์บ็อกซ์ bd6b2ce2d9314e6e.png เพิ่มราคาและตั้งราคาเป็น $1.99 หรือเทียบเท่าในสกุลเงินอื่น 926b03544ae044c4.png เพิ่มการแปลภาษาสำหรับการซื้อ เรียกการซื้อ Spring is in the air โดยใช้ 2000 dashes fly out เป็นคำอธิบาย e26dd4f966dcfece.png เพิ่มภาพหน้าจอของรีวิว เนื้อหาไม่สำคัญเว้นแต่จะมีการส่งผลิตภัณฑ์ไปตรวจสอบ แต่จำเป็นต้องมีเพื่อให้ผลิตภัณฑ์อยู่ในสถานะ "พร้อมส่ง" ซึ่งจำเป็นเมื่อแอปดึงข้อมูลผลิตภัณฑ์จาก App Store 25171bfd6f3a033a.png
  2. ตั้งค่า dash_upgrade_3d เป็นไอเทมที่ซื้อครั้งเดียว ใช้ dash_upgrade_3d เป็นรหัสผลิตภัณฑ์ ตั้งชื่ออ้างอิงเป็น dash upgrade 3d เรียกการซื้อ 3D Dash โดยใช้ Brings your dash back to the future เป็นคำอธิบาย ตั้งราคาเป็น $0.99 กำหนดค่าความพร้อมจำหน่ายสินค้าและอัปโหลดภาพหน้าจอรีวิวในลักษณะเดียวกับdash_consumable_2k ผลิตภัณฑ์ 83878759f32a7d4a.png
  3. ตั้งค่า dash_subscription_doubler เป็นการสมัครใช้บริการแบบต่ออายุอัตโนมัติ ขั้นตอนการสมัครใช้บริการจะแตกต่างออกไปเล็กน้อย ก่อนอื่นคุณต้องสร้างกลุ่มการสมัครใช้บริการ เมื่อการสมัครใช้บริการหลายรายการอยู่ในกลุ่มเดียวกัน ผู้ใช้จะสมัครใช้บริการได้เพียง 1 รายการในเวลาเดียวกัน แต่สามารถอัปเกรดหรือดาวน์เกรดระหว่างการสมัครใช้บริการเหล่านี้ได้ เพียงโทรหา subscriptions 393a44b09f3cd8bf.png และเพิ่มการแปลสำหรับกลุ่มการสมัครใช้บริการ 595aa910776349bd.png จากนั้นคุณจะสร้างการสมัครใช้บริการ ตั้งชื่ออ้างอิงเป็น dash subscription doubler และรหัสผลิตภัณฑ์เป็น dash_subscription_doubler 7bfff7bbe11c8eec.png จากนั้นเลือกระยะเวลาการสมัครใช้บริการ 1 สัปดาห์และการแปล ตั้งชื่อการสมัครใช้บริการนี้ว่า Jet Engine พร้อมคำอธิบาย Doubles your clicks ตั้งราคาเป็น $0.49 กำหนดค่าความพร้อมจำหน่ายสินค้าและอัปโหลดภาพหน้าจอรีวิวในลักษณะเดียวกับdash_consumable_2k ผลิตภัณฑ์ 44d18e02b926a334.png

ตอนนี้คุณควรเห็นผลิตภัณฑ์ในรายการต่อไปนี้

17f242b5c1426b79.png d71da951f595054a.png

5. ตั้งค่า Play Store

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

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

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

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

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

ไฟล์เป็นแบบส่วนตัว อย่าเช็คอินลงในระบบควบคุมแหล่งที่มาแบบสาธารณะ

อ้างอิง 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.kts

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

import java.util.Properties
import java.io.FileInputStream

plugins {
    // omitted
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // omitted
}

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

อัปเดตบล็อก buildTypes เป็น

   buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

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

   signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

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

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

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

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

flutter build appbundle

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

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

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

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

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

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

หากต้องการทดสอบการซื้อในแอป คุณต้องเพิ่มบัญชี 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: การซื้อไอเทมที่ใช้แล้วหมดไปซึ่งซื้อได้หลายครั้ง โดยจะให้ Dash (สกุลเงินในแอป) แก่ผู้ใช้ 2, 000 Dash ต่อการซื้อ
  • dash_upgrade_3d: การซื้อ "การอัปเกรด" แบบใช้ครั้งเดียวซึ่งซื้อได้เพียงครั้งเดียว ซึ่งจะทำให้ผู้ใช้มี Dash ที่แตกต่างกันในเชิงความสวยงามให้คลิก
  • dash_subscription_doubler: การสมัครใช้บริการที่ให้ผู้ใช้ได้รับ Dash ต่อการคลิกเป็น 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

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

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

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

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

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

นอกจากนี้ แอปเริ่มต้นยังมีปลั๊กอิน Firebase ด้วย

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

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

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

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

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

fe2e0933d6810888.png

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

d02d641821c71e2c.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 โดยปกติแล้ว Keystore จะอยู่ในโฟลเดอร์หน้าแรกภายใต้ .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 แล้วกรอกข้อมูลในช่องสุดท้ายในกล่องโต้ตอบแบบโมดัลการส่งแอป

สุดท้าย ให้เรียกใช้คำสั่ง flutterfire configure อีกครั้งเพื่ออัปเดตแอปให้รวมการกำหนดค่าการลงนาม

$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes

ตั้งค่า Firebase สำหรับ iOS: ขั้นตอนเพิ่มเติม

เปิด ios/Runner.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.plist

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

  1. รับค่าสำหรับ REVERSED_CLIENT_ID จากไฟล์ GoogleService-Info.plist โดยไม่มีองค์ประกอบ <string>..</string> ล้อมรอบ
  2. แทนที่ค่าในไฟล์ ios/Runner/Info.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 จะติดตามจำนวน Dash ปัจจุบันและเพิ่มจำนวนโดยอัตโนมัติ 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 ให้ไปที่โค้ดสำหรับ DashPurchasesChangeNotifier ในตอนนี้ คุณเพิ่มได้เฉพาะ DashCounter ที่ซื้อเท่านั้น

เพิ่มพร็อพเพอร์ตี้การสมัครใช้บริการสตรีม _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 ในเครื่องมือสร้าง โปรเจ็กต์นี้ได้รับการตั้งค่าให้เป็นแบบไม่เป็นค่าว่างได้โดยค่าเริ่มต้น (NNBD) ซึ่งหมายความว่าพร็อพเพอร์ตี้ที่ไม่ได้ประกาศให้เป็นค่าว่างได้จะต้องมีค่าที่ไม่ใช่ค่าว่าง late ตัวระบุช่วยให้คุณกำหนดค่านี้ในภายหลังได้

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

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

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.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. ซื้อสินค้าหรือบริการ

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

Adapt 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 = [];

โหลดการซื้อที่พร้อมใช้งาน

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

lib/logic/dash_purchases.dart

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

เมื่อร้านค้าพร้อมใช้งาน ให้โหลดการซื้อที่พร้อมใช้งาน เนื่องจากการตั้งค่า Google Play และ App Store ก่อนหน้านี้ คุณจะเห็น 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);
    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();                                       // Add this line
  }

สุดท้าย ให้เปลี่ยนค่าของช่อง 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);
      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) ไปยังเครื่องที่จะเรียกใช้เซิร์ฟเวอร์ได้ โดยอุปกรณ์ทั้ง 2 เครื่องต้องอยู่ในเครือข่ายเดียวกัน และคุณต้องทราบที่อยู่ 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 Android Developer API ใน Google Cloud Console 629f0bd8e6b50be8.png ในกรณีที่ Google Play Console ขอให้คุณสร้างหรือลิงก์กับโปรเจ็กต์ที่มีอยู่ ให้ดำเนินการดังกล่าวก่อน แล้วกลับมาที่หน้านี้
  2. จากนั้นไปที่หน้าบัญชีบริการ แล้วคลิก + สร้างบัญชีบริการ 8dc97e3b1262328a.png
  3. ป้อนชื่อบัญชีบริการ แล้วคลิกสร้างและดำเนินการต่อ 4fe8106af85ce75f.png
  4. เลือกบทบาทผู้สมัครใช้บริการ Pub/Sub แล้วคลิกเสร็จสิ้น a5b6fa6ea8ee22d.png
  5. เมื่อสร้างบัญชีแล้ว ให้ไปที่จัดการคีย์ eb36da2c1ad6dd06.png
  6. เลือกเพิ่มคีย์ > สร้างคีย์ใหม่ e92db9557a28a479.png
  7. สร้างและดาวน์โหลดคีย์ JSON 711d04f2f4176333.png
  8. เปลี่ยนชื่อไฟล์ที่ดาวน์โหลดเป็น service-account-google-play.json, แล้วย้ายไปไว้ในไดเรกทอรี assets/
  9. จากนั้นไปที่หน้าผู้ใช้และสิทธิ์ใน Play Console28fffbfc35b45f97.png
  10. คลิกเชิญผู้ใช้ใหม่ แล้วป้อนอีเมลของบัญชีบริการที่สร้างไว้ก่อนหน้านี้ คุณดูอีเมลได้ในตารางในหน้าบัญชีบริการe3310cc077f397d.png
  11. ให้สิทธิ์ดูข้อมูลทางการเงินและจัดการคำสั่งซื้อและการสมัครใช้บริการสำหรับแอปพลิเคชัน a3b8cf2b660d1900.png
  12. คลิกเชิญผู้ใช้

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

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

หากต้องการเข้าถึง App Store เพื่อยืนยันการซื้อ คุณต้องตั้งค่าคีย์ลับที่แชร์โดยทำดังนี้

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

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

ก่อนดำเนินการต่อ โปรดตรวจสอบว่าได้กำหนดค่าค่าคงที่ต่อไปนี้ในไฟล์ 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 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart';                  // Add this import
import '../model/firebase_state.dart';                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart';                                  // And this one as well

class PurchasePage extends StatelessWidget {
  const PurchasePage({super.key});

  @override
  Widget build(BuildContext context) {                     // Update from here
    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();
    }                                                      // To here.

    // ...

เรียกใช้ปลายทางการยืนยันการโทรจากแอป

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

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

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

lib/logic/dash_purchases.dart

import 'dart:async';
import 'dart:convert';                                     // Add this import

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;                   // And this import
import 'package:in_app_purchase/in_app_purchase.dart';

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

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;                       // Add this line
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];

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

  final iapConnection = IAPConnection.instance;

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

เพิ่ม 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

  Future<FirebaseFirestore> get firestore async {
    var isInitialized = await _isInitialized.future;
    if (!isInitialized) {
      throw Exception('Firebase is not initialized');
    }
    return FirebaseFirestore.instance;
  }

  User? get user => FirebaseAuth.instance.currentUser;     // Add this line

  Future<void> load() async {
    // ...

เพิ่มฟังก์ชัน _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(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

    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: ข้อมูลเกี่ยวกับผลิตภัณฑ์ คุณจะกำหนดค่านี้ในอีกไม่กี่นาที
  • 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,
  }) async {
    return true;
  }

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

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

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

เปิด bin/server.dart แล้วสร้างปลายทาง API โดยใช้ shelf_route ดังนี้

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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 และดึงข้อมูลต่อไปนี้
    1. userId: รหัสผู้ใช้ที่เข้าสู่ระบบ
    2. source: ร้านค้าที่ใช้ ไม่ว่าจะเป็น app_store หรือ google_play
    3. productData: ได้รับจาก productDataMap ที่คุณสร้างไว้ก่อนหน้านี้
    4. token: มีข้อมูลการยืนยันที่จะส่งไปยังร้านค้า
  3. เรียกใช้เมธอด verifyPurchase สำหรับ GooglePlayPurchaseHandler หรือ AppStorePurchaseHandler ขึ้นอยู่กับแหล่งที่มา
  4. หากการยืนยันสำเร็จ เมธอดจะแสดง Response.ok ต่อไคลเอ็นต์
  5. หากการยืนยันไม่สำเร็จ เมธอดจะแสดงผล Response.internalServerError ให้กับไคลเอ็นต์

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

bin/server.dart

import 'dart:convert';
import 'dart:io'; // new

import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @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 don't 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 don't 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 APIs ซึ่งหมายความว่าคุณสามารถใช้ตรรกะเดียวกันกับทั้ง 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 {

    // See next step
  }

ตอนนี้ ให้ติดตั้งใช้งาน 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. ติดตามการซื้อ

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

ก่อนอื่น ให้ตั้งค่าการประมวลผลเหตุการณ์ของร้านค้าในแบ็กเอนด์ด้วยแบ็กเอนด์ 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 ได้รับการกำหนดค่าให้เรียกใช้เมธอด _pullMessageFromPubSub ทุกๆ 10 วินาที คุณปรับระยะเวลาได้ตามต้องการ

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

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/$googleCloudProjectId/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/$googleCloudProjectId/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

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

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

จากนั้นสร้างอินสแตนซ์ PubsubApi ดังนี้

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

และสุดท้าย ส่งไปยังเครื่องมือสร้าง GooglePlayPurchaseHandler ดังนี้

bin/server.dart

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

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

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

ก่อนอื่น ให้สร้างหัวข้อ Pub/Sub โดยทำดังนี้

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

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

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

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

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

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

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

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,                                  // And this parameter
  );

แก้ไขตัวสร้างเพื่อเพิ่มตัวจับเวลาที่จะเรียกใช้เมธอด _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

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  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

import 'package:app_store_server_sdk/app_store_server_sdk.dart';  // Add this import
import 'package:firebase_backend_dart/constants.dart';            // And this one.


  // 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,                                     // Add this argument
    ),
  };

การตั้งค่า 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((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 ใน Constructor โดยตรง และนำ Listener ออกในเมธอด dispose() ในตอนแรก Listener อาจเป็นเพียงฟังก์ชันว่างก็ได้ เนื่องจาก IAPRepo เป็น ChangeNotifier และคุณเรียกใช้ notifyListeners() ทุกครั้งที่การซื้อใน Firestore เปลี่ยนแปลง ระบบจะเรียกใช้เมธอด purchasesUpdate() เสมอเมื่อผลิตภัณฑ์ที่ซื้อมีการเปลี่ยนแปลง

lib/logic/dash_purchases.dart

import '../repo/iap_repo.dart';                              // Add this import

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];
  IAPRepo iapRepo;                                           // Add this line

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;
  final iapConnection = IAPConnection.instance;

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

  Future<void> loadPurchases() async {
    // Elided.
  }

  @override
  void dispose() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);                 // Add this line
    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>(),                         // Add this line
          ),
          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โฟลเดอร์ complete

ดูข้อมูลเพิ่มเติมได้ที่โค้ดแล็บ Flutter อื่นๆ