1. บทนำ
การเพิ่มการซื้อในแอปไปยังแอป Flutter ต้องตั้งค่า App Store และ Play Store อย่างถูกต้อง ยืนยันการซื้อ และให้สิทธิ์ที่จำเป็น เช่น สิทธิพิเศษสำหรับการสมัครใช้บริการ
ใน Codelab นี้ คุณจะได้เพิ่มการซื้อในแอป 3 ประเภทลงในแอป (ที่เราจัดเตรียมไว้ให้) และยืนยันการซื้อเหล่านี้โดยใช้แบ็กเอนด์ Dart กับ Firebase แอป Dash Clicker ที่ระบุมีเกมที่ใช้มาสคอต Dash เป็นสกุลเงิน คุณจะเพิ่มตัวเลือกการซื้อต่อไปนี้
- ตัวเลือกการซื้อที่ทำซ้ำได้สำหรับ Dash 2000 รายการพร้อมกัน
- การซื้อการอัปเกรดแบบครั้งเดียวเพื่อเปลี่ยนแดชบอร์ดสไตล์เก่าให้เป็นแดชบอร์ดสไตล์ใหม่
- การสมัครใช้บริการที่เพิ่มการคลิกที่สร้างขึ้นโดยอัตโนมัติเป็น 2 เท่า
ตัวเลือกการซื้อครั้งแรกจะให้สิทธิประโยชน์แก่ผู้ใช้โดยตรงเป็น Dash 2,000 รายการ โดยผู้ใช้จะเข้าถึงไอเทมเหล่านี้ได้โดยตรงและซื้อได้หลายครั้ง เราเรียกไอเทมนี้ว่าไอเทมที่ใช้แล้วหมดไป เนื่องจากไอเทมนี้จะถูกใช้โดยตรงและใช้ได้หลายครั้ง
ตัวเลือกที่ 2 จะอัปเกรด Dash ให้สวยงามยิ่งขึ้น คุณต้องซื้อเพียงครั้งเดียวและจะใช้ได้ตลอดไป การซื้อดังกล่าวเรียกว่า "ผลิตภัณฑ์ที่อยู่ตลอดไป" เนื่องจากแอปไม่สามารถใช้ผลิตภัณฑ์ได้ แต่ผลิตภัณฑ์จะใช้งานได้ตลอดไป
ตัวเลือกการซื้อที่ 3 และตัวเลือกสุดท้ายคือการสมัครใช้บริการ ในขณะที่การสมัครใช้บริการมีผล ผู้ใช้จะได้รับ Dash เร็วขึ้น แต่เมื่อหยุดชำระเงินสำหรับการสมัครใช้บริการแล้ว สิทธิประโยชน์ก็จะหายไปด้วย
บริการแบ็กเอนด์ (ซึ่งเราจัดเตรียมไว้ให้คุณด้วย) จะทำงานเป็นแอป Dart โดยจะยืนยันว่ามีการซื้อเกิดขึ้นจริง และจัดเก็บข้อมูลการซื้อโดยใช้ Firestore เราใช้ Firestore เพื่อให้กระบวนการนี้ง่ายขึ้น แต่ในแอปเวอร์ชันที่ใช้งานจริง คุณสามารถใช้บริการแบ็กเอนด์ประเภทใดก็ได้
สิ่งที่คุณจะสร้าง
- คุณจะขยายแอปให้รองรับการซื้อไอเทมที่ใช้แล้วหมดและการสมัครใช้บริการ
- นอกจากนี้ คุณยังจะขยายแอปแบ็กเอนด์ 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
ในโครงสร้างโฟลเดอร์ของ Xcode โปรเจ็กต์ Runner จะอยู่ด้านบน และเป้าหมาย Flutter, Runner และ Products จะอยู่ใต้โปรเจ็กต์ Runner ดับเบิลคลิก Runner เพื่อแก้ไขการตั้งค่าโปรเจ็กต์ แล้วคลิก Signing & Capabilities ป้อนตัวระบุ Bundle ที่คุณเพิ่งเลือกในช่องทีมเพื่อตั้งค่าทีม
ตอนนี้คุณสามารถปิด Xcode และกลับไปที่ Android Studio เพื่อกำหนดค่าสำหรับ Android ให้เสร็จได้แล้ว โดยเปิดbuild.gradle.kts
ในส่วนandroid/app,
แล้วเปลี่ยนapplicationId
(ในบรรทัดที่ 24 ในภาพหน้าจอด้านล่าง) เป็นรหัสแอปพลิเคชัน ซึ่งเหมือนกับตัวระบุชุดของ iOS โปรดทราบว่า ID สำหรับ App Store ของ iOS และ Android ไม่จำเป็นต้องเหมือนกัน แต่การใช้ ID ที่เหมือนกันจะช่วยลดข้อผิดพลาดได้ ดังนั้นใน Codelab นี้เราจะใช้ตัวระบุที่เหมือนกันด้วย
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/ แล้วคลิกข้อตกลง ภาษี และการธนาคาร
คุณจะเห็นข้อตกลงสำหรับแอปฟรีและแอปแบบชำระเงินที่นี่ สถานะของแอปฟรีควรเป็น "ใช้งานอยู่" และสถานะของแอปแบบชำระเงินคือ "ใหม่" โปรดตรวจสอบว่าคุณได้อ่านข้อกำหนด ยอมรับข้อกำหนด และป้อนข้อมูลที่จำเป็นทั้งหมด
เมื่อตั้งค่าทุกอย่างถูกต้องแล้ว สถานะของแอปที่ต้องซื้อจะ "ใช้งานอยู่" ซึ่งเป็นเรื่องสำคัญมากเนื่องจากคุณจะไม่สามารถลองซื้อในแอปได้หากไม่มีข้อตกลงที่ใช้งานอยู่
ลงทะเบียนรหัสแอป
สร้างตัวระบุใหม่ในพอร์ทัลนักพัฒนาแอปของ Apple ไปที่ developer.apple.com/account/resources/identifiers/list แล้วคลิกไอคอน "บวก" ข้างส่วนหัวตัวระบุ
เลือกรหัสแอป
เลือกแอป
ระบุคำอธิบายและตั้งค่ารหัสชุดให้ตรงกับรหัสชุดเป็นค่าเดียวกับที่ตั้งไว้ก่อนหน้านี้ใน XCode
ดูคำแนะนำเพิ่มเติมเกี่ยวกับวิธีสร้างรหัสแอปใหม่ได้ในความช่วยเหลือเกี่ยวกับบัญชีนักพัฒนาแอป
การสร้างแอปใหม่
สร้างแอปใหม่ใน App Store Connect โดยใช้ตัวระบุชุดที่ไม่ซ้ำกัน
ดูคำแนะนำเพิ่มเติมเกี่ยวกับวิธีสร้างแอปใหม่และจัดการข้อตกลงได้ที่ความช่วยเหลือของ App Store Connect
หากต้องการทดสอบการซื้อในแอป คุณต้องมีผู้ใช้ทดสอบใน Sandbox ผู้ใช้ทดสอบนี้ไม่ควรเชื่อมต่อกับ iTunes แต่ใช้สำหรับการทดสอบการซื้อในแอปเท่านั้น คุณจะใช้อีเมลที่ใช้สำหรับบัญชี Apple อยู่แล้วไม่ได้ ในผู้ใช้และการเข้าถึง ให้ไปที่แซนด์บ็อกซ์เพื่อสร้างบัญชีแซนด์บ็อกซ์ใหม่หรือจัดการ Apple ID แซนด์บ็อกซ์ที่มีอยู่
ตอนนี้คุณตั้งค่าผู้ใช้ Sandbox ใน iPhone ได้โดยไปที่การตั้งค่า > นักพัฒนาแอป > บัญชี Apple Sandbox
การกำหนดค่าการซื้อในแอป
ตอนนี้คุณจะกำหนดค่าไอเทมที่ซื้อได้ 3 รายการดังนี้
dash_consumable_2k
: การซื้อไอเทมที่ใช้แล้วหมดไปซึ่งซื้อได้หลายครั้ง โดยจะให้ Dash (สกุลเงินในแอป) แก่ผู้ใช้ 2, 000 Dash ต่อการซื้อdash_upgrade_3d
: การซื้อ "การอัปเกรด" แบบใช้ครั้งเดียวซึ่งซื้อได้เพียงครั้งเดียว และทำให้ผู้ใช้มี Dash ที่แตกต่างกันในเชิงความสวยงามให้คลิกdash_subscription_doubler
: การสมัครใช้บริการที่ให้ผู้ใช้ได้รับ Dash ต่อการคลิกเป็น 2 เท่าตลอดระยะเวลาการสมัครใช้บริการ
ไปที่การซื้อในแอป
สร้างการซื้อในแอปด้วยรหัสที่ระบุ
- ตั้งค่า
dash_consumable_2k
เป็นไอเทมสิ้นเปลือง ใช้dash_consumable_2k
เป็นรหัสผลิตภัณฑ์ ชื่ออ้างอิงจะใช้ใน App Store Connect เท่านั้น เพียงตั้งค่าเป็นdash consumable 2k
ตั้งค่าความพร้อมให้บริการ ผลิตภัณฑ์ต้องพร้อมให้บริการในประเทศของผู้ใช้แซนด์บ็อกซ์
เพิ่มราคาและตั้งราคาเป็น
$1.99
หรือเทียบเท่าในสกุลเงินอื่นเพิ่มการแปลภาษาสำหรับการซื้อ เรียกการซื้อ
Spring is in the air
โดยใช้2000 dashes fly out
เป็นคำอธิบายเพิ่มภาพหน้าจอของรีวิว เนื้อหาไม่สำคัญเว้นแต่จะมีการส่งผลิตภัณฑ์ไปตรวจสอบ แต่จำเป็นต้องมีเพื่อให้ผลิตภัณฑ์อยู่ในสถานะ "พร้อมส่ง" ซึ่งจำเป็นเมื่อแอปดึงข้อมูลผลิตภัณฑ์จาก App Store
- ตั้งค่า
dash_upgrade_3d
เป็นไอเทมที่ซื้อครั้งเดียว ใช้dash_upgrade_3d
เป็นรหัสผลิตภัณฑ์ ตั้งชื่ออ้างอิงเป็นdash upgrade 3d
เรียกการซื้อ3D Dash
โดยใช้Brings your dash back to the future
เป็นคำอธิบาย ตั้งราคาเป็น$0.99
กำหนดค่าความพร้อมจำหน่ายสินค้าและอัปโหลดภาพหน้าจอรีวิวในลักษณะเดียวกับdash_consumable_2k
ผลิตภัณฑ์ - ตั้งค่า
dash_subscription_doubler
เป็นการสมัครใช้บริการแบบต่ออายุอัตโนมัติ ขั้นตอนการสมัครใช้บริการจะแตกต่างออกไปเล็กน้อย ก่อนอื่นคุณต้องสร้างกลุ่มการสมัครใช้บริการ เมื่อการสมัครใช้บริการหลายรายการอยู่ในกลุ่มเดียวกัน ผู้ใช้จะสมัครใช้บริการได้เพียง 1 รายการในเวลาเดียวกัน แต่สามารถอัปเกรดหรือดาวน์เกรดระหว่างการสมัครใช้บริการเหล่านี้ได้ เพียงโทรหาsubscriptions
และเพิ่มการแปลสำหรับกลุ่มการสมัครใช้บริการ
จากนั้นคุณจะสร้างการสมัครใช้บริการ ตั้งชื่ออ้างอิงเป็น
dash subscription doubler
และรหัสผลิตภัณฑ์เป็นdash_subscription_doubler
จากนั้นเลือกระยะเวลาการสมัครใช้บริการ 1 สัปดาห์และการแปล ตั้งชื่อการสมัครใช้บริการนี้ว่า
Jet Engine
พร้อมคำอธิบายDoubles your clicks
ตั้งราคาเป็น$0.49
กำหนดค่าความพร้อมจำหน่ายสินค้าและอัปโหลดภาพหน้าจอรีวิวในลักษณะเดียวกับdash_consumable_2k
ผลิตภัณฑ์
ตอนนี้คุณควรเห็นผลิตภัณฑ์ในรายการต่อไปนี้
5. ตั้งค่า Play Store
เช่นเดียวกับ App Store คุณจะต้องมีบัญชีนักพัฒนาแอปสำหรับ Play Store ด้วย หากยังไม่มี ให้ลงทะเบียนบัญชี
สร้างแอปใหม่
สร้างแอปใหม่ใน Google Play Console โดยทำดังนี้
- เปิด Play Console
- เลือกแอปทั้งหมด > สร้างแอป
- เลือกภาษาเริ่มต้นแล้วเพิ่มชื่อแอป พิมพ์ชื่อแอปที่คุณต้องการให้แสดงใน Google Play คุณเปลี่ยนชื่อได้ในภายหลัง
- ระบุว่าแอปพลิเคชันของคุณเป็นเกม คุณเปลี่ยนข้อมูลนี้ได้ในภายหลัง
- ระบุว่าแอปพลิเคชันของคุณเป็นแบบฟรีหรือต้องซื้อ
- ปฏิบัติตามประกาศหลักเกณฑ์ด้านเนื้อหาและกฎหมายการส่งออกของสหรัฐอเมริกาให้ครบถ้วน
- เลือกสร้างแอป
หลังจากสร้างแอปแล้ว ให้ไปที่แดชบอร์ดและทํางานทั้งหมดในส่วนตั้งค่าแอปให้เสร็จสมบูรณ์ ในส่วนนี้ คุณจะต้องระบุข้อมูลบางอย่างเกี่ยวกับแอป เช่น การจัดประเภทเนื้อหาและภาพหน้าจอ
ลงนามในใบสมัคร
คุณต้องอัปโหลดบิลด์อย่างน้อย 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 ตำแหน่งต่อไปนี้
- ไปยังแทร็กทดสอบที่เฉพาะเจาะจง (การทดสอบภายใน)
- ในฐานะผู้ทดสอบที่มีใบอนุญาต
ก่อนอื่น ให้เริ่มด้วยการเพิ่มผู้ทดสอบลงในแทร็กทดสอบภายใน กลับไปที่ทดสอบและเผยแพร่ > การทดสอบ > การทดสอบภายใน แล้วคลิกแท็บผู้ทดสอบ
สร้างรายชื่ออีเมลใหม่โดยคลิกสร้างรายชื่ออีเมล ตั้งชื่อรายการ แล้วเพิ่มอีเมลของบัญชี Google ที่ต้องมีสิทธิ์เข้าถึงการทดสอบการซื้อในแอป
จากนั้นเลือกช่องทำเครื่องหมายสำหรับรายการ แล้วคลิกบันทึกการเปลี่ยนแปลง
จากนั้นเพิ่มผู้ทดสอบที่มีใบอนุญาตโดยทำดังนี้
- กลับไปที่มุมมองแอปทั้งหมดของ Google Play Console
- ไปที่การตั้งค่า > การทดสอบใบอนุญาต
- เพิ่มอีเมลของผู้ทดสอบที่ต้องทดสอบการซื้อในแอป
- ตั้งค่าการตอบกลับใบอนุญาตเป็น
RESPOND_NORMALLY
- คลิกบันทึกการเปลี่ยนแปลง
การกำหนดค่าการซื้อในแอป
ตอนนี้คุณจะกำหนดค่าไอเทมที่ซื้อได้ภายในแอป
คุณต้องกำหนดการซื้อที่แตกต่างกัน 3 รายการเช่นเดียวกับใน App Store ดังนี้
dash_consumable_2k
: การซื้อไอเทมที่ใช้แล้วหมดไปซึ่งซื้อได้หลายครั้ง โดยจะให้ Dash (สกุลเงินในแอป) แก่ผู้ใช้ 2, 000 Dash ต่อการซื้อdash_upgrade_3d
: การซื้อ "การอัปเกรด" แบบใช้ครั้งเดียวซึ่งซื้อได้เพียงครั้งเดียว ซึ่งจะทำให้ผู้ใช้มี Dash ที่แตกต่างกันในเชิงความสวยงามให้คลิกdash_subscription_doubler
: การสมัครใช้บริการที่ให้ผู้ใช้ได้รับ Dash ต่อการคลิกเป็น 2 เท่าตลอดระยะเวลาการสมัครใช้บริการ
ก่อนอื่นให้เพิ่มไอเทมแบบใช้แล้วหมดไปและแบบที่อยู่ตลอดไป
- ไปที่ Google Play Console แล้วเลือกแอปพลิเคชัน
- ไปที่สร้างรายได้ > ผลิตภัณฑ์ > ไอเทมที่ซื้อในแอป
- คลิกสร้างผลิตภัณฑ์
- ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับผลิตภัณฑ์ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณต้องการใช้
- คลิกบันทึก
- คลิกเปิดใช้งาน
- ทำกระบวนการเดิมซ้ำสำหรับการซื้อ "อัปเกรด" แบบใช้ครั้งเดียว
จากนั้นให้เพิ่มการสมัครใช้บริการโดยทำดังนี้
- ไปที่ Google Play Console แล้วเลือกแอปพลิเคชัน
- ไปที่สร้างรายได้ > ผลิตภัณฑ์ > การสมัครใช้บริการ
- คลิกสร้างการสมัครใช้บริการ
- ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับการสมัครใช้บริการ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณต้องการใช้ทุกประการ
- คลิกบันทึก
ตอนนี้คุณควรตั้งค่าการซื้อใน Play Console แล้ว
6. ตั้งค่า Firebase
ในโค้ดแล็บนี้ คุณจะได้ใช้บริการแบ็กเอนด์เพื่อยืนยันและติดตามการซื้อของผู้ใช้
การใช้บริการแบ็กเอนด์มีประโยชน์หลายประการ ดังนี้
- คุณยืนยันธุรกรรมได้อย่างปลอดภัย
- คุณสามารถตอบสนองต่อเหตุการณ์การเรียกเก็บเงินจาก App Store ได้
- คุณสามารถติดตามการซื้อในฐานข้อมูลได้
- ผู้ใช้จะไม่สามารถหลอกแอปให้แสดงฟีเจอร์พรีเมียมได้ด้วยการย้อนเวลานาฬิกาของระบบ
แม้ว่าจะมีหลายวิธีในการตั้งค่าบริการแบ็กเอนด์ แต่คุณจะดำเนินการนี้โดยใช้ฟังก์ชัน Cloud และ Firestore โดยใช้ Firebase ของ Google เอง
การเขียนแบ็กเอนด์ถือว่าอยู่นอกขอบเขตของโค้ดแล็บนี้ ดังนั้นโค้ดเริ่มต้นจึงมีโปรเจ็กต์ Firebase ที่จัดการการซื้อขั้นพื้นฐานเพื่อช่วยให้คุณเริ่มต้นใช้งานได้
นอกจากนี้ แอปเริ่มต้นยังมีปลั๊กอิน Firebase ด้วย
สิ่งที่คุณต้องทำคือสร้างโปรเจ็กต์ Firebase ของคุณเอง กำหนดค่าทั้งแอปและแบ็กเอนด์สำหรับ Firebase และสุดท้ายคือติดตั้งใช้งานแบ็กเอนด์
สร้างโปรเจ็กต์ Firebase
ไปที่คอนโซล Firebase แล้วสร้างโปรเจ็กต์ Firebase ใหม่ สำหรับตัวอย่างนี้ ให้ตั้งชื่อโปรเจ็กต์ว่า Dash Clicker
ในแอปแบ็กเอนด์ คุณจะเชื่อมโยงการซื้อกับผู้ใช้ที่เฉพาะเจาะจง ดังนั้นคุณจึงต้องมีการตรวจสอบสิทธิ์ โดยให้ใช้โมดูลการตรวจสอบสิทธิ์ของ Firebase กับการลงชื่อเข้าใช้ด้วย Google
- จากแดชบอร์ด Firebase ให้ไปที่การตรวจสอบสิทธิ์และเปิดใช้หากจำเป็น
- ไปที่แท็บวิธีการลงชื่อเข้าใช้ แล้วเปิดใช้ผู้ให้บริการลงชื่อเข้าใช้ Google
เนื่องจากคุณจะใช้ฐานข้อมูล Firestore ของ Firebase ด้วย ให้เปิดใช้ตัวเลือกนี้ด้วย
ตั้งค่ากฎ 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)
หากต้องการอนุญาตการลงชื่อเข้าใช้ด้วย 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
มีการเพิ่มคู่คีย์-ค่าแล้ว แต่ต้องแทนที่ค่าของคู่คีย์-ค่าดังกล่าว
- รับค่าสำหรับ
REVERSED_CLIENT_ID
จากไฟล์GoogleService-Info.plist
โดยไม่มีองค์ประกอบ<string>..</string>
ล้อมรอบ - แทนที่ค่าในไฟล์
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 หากกำหนดค่าอย่างถูกต้อง โปรดทราบว่าระบบอาจใช้เวลาสักครู่ก่อนที่การซื้อจะพร้อมใช้งานเมื่อป้อนลงในคอนโซลที่เกี่ยวข้อง
กลับไปที่ 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/
เป็นรูท
ตรวจสอบว่าคุณได้ติดตั้งเครื่องมือต่อไปนี้
- Dart
- Firebase CLI
ภาพรวมโปรเจ็กต์พื้นฐาน
เนื่องจากบางส่วนของโปรเจ็กต์นี้ถือว่าอยู่นอกขอบเขตของโค้ดแล็บนี้ เราจึงรวมไว้ในโค้ดเริ่มต้น คุณควรดูโค้ดเริ่มต้นที่มีอยู่แล้วก่อนเริ่ม เพื่อให้ทราบว่าคุณจะจัดโครงสร้างสิ่งต่างๆ อย่างไร
โค้ดแบ็กเอนด์นี้สามารถเรียกใช้ในเครื่องของคุณได้โดยไม่ต้องติดตั้งใช้งาน อย่างไรก็ตาม คุณต้องเชื่อมต่อจากอุปกรณ์ที่ใช้พัฒนา (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 แล้วไปที่ส่วนบัญชีบริการ จากนั้นเลือกสร้างคีย์ส่วนตัวใหม่
คัดลอกไฟล์ JSON ที่ดาวน์โหลดไปยังโฟลเดอร์ assets/
แล้วเปลี่ยนชื่อเป็น service-account-firebase.json
ตั้งค่าการเข้าถึง Google Play
หากต้องการเข้าถึง Play Store เพื่อยืนยันการซื้อ คุณต้องสร้างบัญชีบริการที่มีสิทธิ์เหล่านี้ แล้วดาวน์โหลดข้อมูลเข้าสู่ระบบ JSON สำหรับบัญชีดังกล่าว
- ไปที่หน้า Google Play Android Developer API ใน Google Cloud Console
ในกรณีที่ Google Play Console ขอให้คุณสร้างหรือลิงก์กับโปรเจ็กต์ที่มีอยู่ ให้ดำเนินการดังกล่าวก่อน แล้วกลับมาที่หน้านี้
- จากนั้นไปที่หน้าบัญชีบริการ แล้วคลิก + สร้างบัญชีบริการ
- ป้อนชื่อบัญชีบริการ แล้วคลิกสร้างและดำเนินการต่อ
- เลือกบทบาทผู้สมัครใช้บริการ Pub/Sub แล้วคลิกเสร็จสิ้น
- เมื่อสร้างบัญชีแล้ว ให้ไปที่จัดการคีย์
- เลือกเพิ่มคีย์ > สร้างคีย์ใหม่
- สร้างและดาวน์โหลดคีย์ JSON
- เปลี่ยนชื่อไฟล์ที่ดาวน์โหลดเป็น
service-account-google-play.json,
แล้วย้ายไปไว้ในไดเรกทอรีassets/
- จากนั้นไปที่หน้าผู้ใช้และสิทธิ์ใน Play Console
- คลิกเชิญผู้ใช้ใหม่ แล้วป้อนอีเมลของบัญชีบริการที่สร้างไว้ก่อนหน้านี้ คุณดูอีเมลได้ในตารางในหน้าบัญชีบริการ
- ให้สิทธิ์ดูข้อมูลทางการเงินและจัดการคำสั่งซื้อและการสมัครใช้บริการสำหรับแอปพลิเคชัน
- คลิกเชิญผู้ใช้
อีกสิ่งหนึ่งที่เราต้องทำคือเปิด lib/constants.dart,
และแทนที่ค่าของ androidPackageId
ด้วยรหัสแพ็กเกจที่คุณเลือกสำหรับแอป Android
ตั้งค่าการเข้าถึง Apple App Store
หากต้องการเข้าถึง App Store เพื่อยืนยันการซื้อ คุณต้องตั้งค่าคีย์ลับที่แชร์โดยทำดังนี้
- เปิด App Store Connect
- ไปที่แอปของฉัน แล้วเลือกแอป
- ไปที่ทั่วไป > ข้อมูลแอปในการนำทางแถบด้านข้าง
- คลิกจัดการในส่วนหัวข้อมูลลับที่แชร์เฉพาะแอป
- สร้างรหัสลับใหม่แล้วคัดลอก
- เปิด
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
แบบนามธรรมที่มีการติดตั้งใช้งานแยกกันสำหรับแต่ละร้านค้า
เริ่มต้นด้วยการเพิ่มไฟล์ 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');
}
}
โค้ดจะทำสิ่งต่อไปนี้
- กำหนดปลายทาง POST ที่จะเรียกใช้จากแอปที่คุณสร้างไว้ก่อนหน้านี้
- ถอดรหัสเพย์โหลด JSON และดึงข้อมูลต่อไปนี้
userId
: รหัสผู้ใช้ที่เข้าสู่ระบบsource
: ร้านค้าที่ใช้ ไม่ว่าจะเป็นapp_store
หรือgoogle_play
productData
: ได้รับจากproductDataMap
ที่คุณสร้างไว้ก่อนหน้านี้token
: มีข้อมูลการยืนยันที่จะส่งไปยังร้านค้า
- เรียกใช้เมธอด
verifyPurchase
สำหรับGooglePlayPurchaseHandler
หรือAppStorePurchaseHandler
ขึ้นอยู่กับแหล่งที่มา - หากการยืนยันสำเร็จ เมธอดจะแสดง
Response.ok
ต่อไคลเอ็นต์ - หากการยืนยันไม่สำเร็จ เมธอดจะแสดงผล
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 โดยทำดังนี้
- ตั้งค่า
googleCloudProjectId
ในconstants.dart
เป็นรหัสของโปรเจ็กต์ Google Cloud - ไปที่หน้า Cloud Pub/Sub ใน Google Cloud Console
- ตรวจสอบว่าคุณอยู่ในโปรเจ็กต์ Firebase แล้วคลิก + สร้างหัวข้อ
- ตั้งชื่อหัวข้อใหม่ให้เหมือนกับค่าที่ตั้งไว้สำหรับ
googlePlayPubsubBillingTopic
ในconstants.dart
ในกรณีนี้ ให้ตั้งชื่อว่าplay_billing
หากเลือกอย่างอื่น โปรดอย่าลืมอัปเดตconstants.dart
สร้างหัวข้อ - ในรายการหัวข้อ Pub/Sub ให้คลิกจุดแนวตั้ง 3 จุดสำหรับหัวข้อที่คุณเพิ่งสร้าง แล้วคลิกดูสิทธิ์
- เลือกเพิ่มหลักในแถบด้านข้างทางด้านขวา
- ในส่วนนี้ ให้เพิ่ม
google-play-developer-notifications@system.gserviceaccount.com
และมอบบทบาทผู้เผยแพร่ Pub/Sub ให้กับ - บันทึกการเปลี่ยนแปลงสิทธิ์
- คัดลอกชื่อหัวข้อของหัวข้อที่คุณเพิ่งสร้าง
- เปิด Play Console อีกครั้ง แล้วเลือกแอปจากรายการแอปทั้งหมด
- เลื่อนลงแล้วไปที่สร้างรายได้ > การตั้งค่าการสร้างรายได้
- กรอกหัวข้อทั้งหมดและบันทึกการเปลี่ยนแปลง
ตอนนี้ระบบจะเผยแพร่เหตุการณ์การเรียกเก็บเงินทั้งหมดของ 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,
),
);
}
}
}
}
วิธีการนี้ทำงานดังนี้
- รับรายการการสมัครใช้บริการที่ใช้งานอยู่จาก Firestore โดยใช้ IapRepository
- สำหรับแต่ละคำสั่งซื้อ ระบบจะขอสถานะการสมัครใช้บริการไปยัง App Store Server API
- รับธุรกรรมล่าสุดสำหรับการซื้อการสมัครใช้บริการนั้น
- ตรวจสอบวันที่หมดอายุ
- อัปเดตสถานะการสมัครใช้บริการใน 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 ดังนี้
- เข้าสู่ระบบ App Store Connect แล้วเลือกผู้ใช้และการเข้าถึง
- ไปที่การผสานรวม > คีย์ > การซื้อในแอป
- แตะไอคอน "บวก" เพื่อเพิ่มรายการใหม่
- ตั้งชื่อ เช่น "คีย์ Codelab"
- ดาวน์โหลดไฟล์ p8 ที่มีคีย์
- คัดลอกไปยังโฟลเดอร์ชิ้นงานโดยใช้ชื่อ
SubscriptionKey.p8
- คัดลอกรหัสคีย์จากคีย์ที่สร้างขึ้นใหม่ แล้วตั้งค่าเป็นค่าคงที่
appStoreKeyId
ในไฟล์lib/constants.dart
- คัดลอกรหัสผู้ออกใบรับรองที่ด้านบนสุดของรายการคีย์ แล้วตั้งค่าเป็นค่าคงที่
appStoreIssuerId
ในไฟล์lib/constants.dart
ติดตามการซื้อในอุปกรณ์
วิธีที่ปลอดภัยที่สุดในการติดตามการซื้อคือการใช้ฝั่งเซิร์ฟเวอร์ เนื่องจากฝั่งไคลเอ็นต์รักษาความปลอดภัยได้ยาก แต่คุณต้องมีวิธีนำข้อมูลกลับไปยังไคลเอ็นต์เพื่อให้แอปดำเนินการกับข้อมูลสถานะการสมัครใช้บริการได้ การจัดเก็บการซื้อใน 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 เสร็จแล้ว คุณดูโค้ดที่เสร็จสมบูรณ์สำหรับโค้ดแล็บนี้ได้ในโฟลเดอร์ complete
ดูข้อมูลเพิ่มเติมได้ที่โค้ดแล็บ Flutter อื่นๆ