1. บทนำ
การเพิ่มการซื้อในแอปลงในแอป Flutter จำเป็นต้องตั้งค่า App Store และ Play Store อย่างถูกต้อง ยืนยันการซื้อ และมอบสิทธิ์ที่จำเป็น เช่น สิทธิพิเศษสำหรับการสมัครใช้บริการ
ในโค้ดแล็บนี้ คุณจะเพิ่มการซื้อในแอป 3 ประเภทลงในแอป (มีให้แล้ว) และยืนยันการซื้อเหล่านี้โดยใช้แบ็กเอนด์ Dart กับ Firebase แอป Dash Clicker ที่ระบุมีเกมที่ใช้มาสคอต Dash เป็นสกุลเงิน คุณจะเพิ่มตัวเลือกการซื้อต่อไปนี้
- ตัวเลือกการซื้อ Dash 2,000 รายการพร้อมกันแบบซ้ำได้
- การซื้อการอัปเกรดแบบครั้งเดียวเพื่อเปลี่ยน Dash แบบเก่าให้เป็น Dash แบบทันสมัย
- การสมัครใช้บริการที่เพิ่มจำนวนคลิกที่สร้างขึ้นโดยอัตโนมัติเป็น 2 เท่า
ตัวเลือกการซื้อครั้งแรกจะให้สิทธิประโยชน์ 2,000 แต้มแก่ผู้ใช้โดยตรง ไอเทมเหล่านี้พร้อมให้บริการแก่ผู้ใช้โดยตรงและซื้อได้หลายครั้ง ประเภทนี้เรียกว่า "เนื้อหาที่บริโภคได้" เนื่องจากมีการบริโภคโดยตรงและบริโภคได้หลายครั้ง
ตัวเลือกที่ 2 จะอัปเกรด Dash ให้สวยงามยิ่งขึ้น โดยคุณจะต้องซื้อเพียงครั้งเดียวและใช้งานได้ตลอดไป การซื้อดังกล่าวเรียกว่า "ซื้อแบบใช้ไม่ได้" เนื่องจากแอปไม่สามารถใช้การซื้อดังกล่าวได้ แต่การซื้อดังกล่าวจะใช้ได้ตลอดไป
ตัวเลือกการซื้อที่ 3 และเป็นตัวเลือกสุดท้ายคือการสมัครใช้บริการ ขณะสมัครใช้บริการอยู่ ผู้ใช้จะได้รับ Dashes เร็วขึ้น แต่เมื่อหยุดชำระเงินค่าสมัครใช้บริการ สิทธิประโยชน์ก็จะหมดไปด้วย
บริการแบ็กเอนด์ (มีให้ใช้งานด้วย) จะทำงานเป็นแอป Dart, ยืนยันว่ามีการซื้อเกิดขึ้น และจัดเก็บโดยใช้ Firestore เราใช้ Firestore เพื่อให้กระบวนการนี้ง่ายขึ้น แต่คุณใช้บริการแบ็กเอนด์ประเภทใดก็ได้ในแอปเวอร์ชันที่ใช้งานจริง
สิ่งที่คุณจะสร้าง
- คุณจะขยายแอปให้รองรับการซื้อและการสมัครใช้บริการแบบใช้แล้วหมด
- นอกจากนี้ คุณยังขยายแอปแบ็กเอนด์ Dart เพื่อยืนยันและจัดเก็บรายการที่ซื้อด้วย
สิ่งที่คุณจะ ได้เรียนรู้
- วิธีกำหนดค่า App Store และ Play Store ด้วยผลิตภัณฑ์ที่ซื้อได้
- วิธีสื่อสารกับร้านค้าเพื่อยืนยันการซื้อและจัดเก็บไว้ใน Firestore
- วิธีจัดการการซื้อในแอป
สิ่งที่คุณต้องมี
- Android Studio 4.1 ขึ้นไป
- Xcode 12 ขึ้นไป (สําหรับการพัฒนา iOS)
- Flutter SDK
2. ตั้งค่าสภาพแวดล้อมการพัฒนา
หากต้องการเริ่มใช้งาน Codelab นี้ ให้ดาวน์โหลดโค้ดและเปลี่ยนรหัสชุดสำหรับ iOS และชื่อแพ็กเกจสำหรับ Android
ดาวน์โหลดรหัส
หากต้องการโคลนที่เก็บ GitHub จากบรรทัดคำสั่ง ให้ใช้คำสั่งต่อไปนี้
git clone https://github.com/flutter/codelabs.git flutter-codelabs
หรือหากติดตั้งเครื่องมือ cli ของ GitHub ไว้แล้ว ให้ใช้คำสั่งต่อไปนี้
gh repo clone flutter/codelabs flutter-codelabs
ระบบจะโคลนโค้ดตัวอย่างไปยังไดเรกทอรี flutter-codelabs
ที่มีโค้ดสําหรับคอลเล็กชันโค้ดแล็บ รหัสของ Codelab นี้อยู่ใน flutter-codelabs/in_app_purchases
โครงสร้างไดเรกทอรีในส่วน flutter-codelabs/in_app_purchases
มีชุดภาพรวมของตำแหน่งที่คุณควรอยู่เมื่อสิ้นสุดแต่ละขั้นตอนที่มีชื่อ รหัสเริ่มต้นอยู่ในขั้นตอนที่ 0 ดังนั้นการค้นหาไฟล์ที่ตรงกันจึงทำได้ง่ายดังนี้
cd flutter-codelabs/in_app_purchases/step_00
หากต้องการข้ามไปข้างหน้าหรือดูว่าภาพควรมีลักษณะอย่างไรหลังจากทำตามขั้นตอนหนึ่งๆ ให้ดูในไดเรกทอรีที่ตั้งชื่อตามขั้นตอนที่คุณสนใจ โค้ดของขั้นตอนสุดท้ายจะอยู่ภายใต้โฟลเดอร์ complete
สร้างโปรเจ็กต์เริ่มต้น
เปิดโปรเจ็กต์เริ่มต้นจาก step_00
ใน IDE ที่คุณชื่นชอบ เราใช้ Android Studio ในการจับภาพหน้าจอ แต่ Visual Studio Code ก็เป็นอีกตัวเลือกที่ยอดเยี่ยม เมื่อใช้เครื่องมือแก้ไขใดก็ตาม โปรดตรวจสอบว่าได้ติดตั้งปลั๊กอิน Dart และ Flutter เวอร์ชันล่าสุดแล้ว
แอปที่คุณกำลังจะสร้างต้องสื่อสารกับ App Store และ Play Store เพื่อดูว่าผลิตภัณฑ์ใดพร้อมจำหน่ายและราคาเท่าใด แอปทุกแอปจะระบุด้วยรหัสที่ไม่ซ้ำกัน สําหรับ App Store ของ iOS รหัสนี้เรียกว่าตัวระบุกลุ่มแอป ส่วนสําหรับ Play Store ของ Android รหัสนี้เรียกว่ารหัสแอปพลิเคชัน โดยปกติแล้วตัวระบุเหล่านี้จะสร้างขึ้นโดยใช้การเขียนชื่อโดเมนแบบย้อนกลับ เช่น เมื่อสร้างแอปการซื้อในแอปสำหรับ flutter.dev คุณจะใช้ dev.flutter.inapppurchase
คิดหาตัวระบุสําหรับแอปของคุณ ตอนนี้คุณจะต้องตั้งค่าตัวระบุดังกล่าวในการตั้งค่าโปรเจ็กต์
ก่อนอื่น ให้ตั้งค่าตัวระบุกลุ่มสําหรับ iOS
เมื่อเปิดโปรเจ็กต์ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ iOS แล้วคลิก Flutter จากนั้นเปิดโมดูลในแอป Xcode
ในโครงสร้างโฟลเดอร์ของ Xcode โปรเจ็กต์ Runner จะอยู่ที่ด้านบน ส่วนเป้าหมาย Flutter, Runner และ Products จะอยู่ใต้โปรเจ็กต์ Runner คลิกสองครั้งที่ Runner เพื่อแก้ไขการตั้งค่าโปรเจ็กต์ แล้วคลิกการรับรองและความสามารถ ป้อนตัวระบุกลุ่มที่เพิ่งเลือกไว้ในช่องทีมเพื่อตั้งค่าทีม
ตอนนี้คุณปิด Xcode และกลับไปที่ Android Studio เพื่อกำหนดค่าสำหรับ Android ให้เสร็จสิ้นได้แล้ว โดยเปิดไฟล์ build.gradle
ในส่วน android/app,
แล้วเปลี่ยน applicationId
(ในบรรทัด 37 ในภาพหน้าจอด้านล่าง) เป็นรหัสแอปพลิเคชัน ซึ่งเหมือนกับตัวระบุกลุ่มของ iOS โปรดทราบว่ารหัสสำหรับ App Store ของ iOS และ Android ไม่จำเป็นต้องเหมือนกัน แต่การใช้รหัสที่เหมือนกันจะทำให้เกิดข้อผิดพลาดน้อยลง ดังนั้นในโค้ดแล็บนี้เราจะใช้ตัวระบุที่เหมือนกันด้วย
3. ติดตั้งปลั๊กอิน
ในส่วนนี้ของโค้ดแล็บ คุณจะติดตั้งปลั๊กอิน in_app_purchase
เพิ่มข้อกำหนดใน pubspec
เพิ่ม in_app_purchase
ลงใน pubspec โดยเพิ่ม in_app_purchase
ลงใน Dependencies ใน pubspec
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
เปิด pubspec.yaml
และตรวจสอบว่าคุณมี in_app_purchase
แสดงเป็นรายการใต้ dependencies
และ in_app_purchase_platform_interface
ใต้ dev_dependencies
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
คลิก pub get เพื่อดาวน์โหลดแพ็กเกจหรือเรียกใช้ flutter pub get
ในบรรทัดคำสั่ง
4. ตั้งค่า App Store
หากต้องการตั้งค่าการซื้อในแอปและทดสอบใน iOS คุณจะต้องสร้างแอปใหม่ใน App Store และสร้างไอเทมที่ซื้อได้ในแอป คุณไม่จำเป็นต้องเผยแพร่หรือส่งแอปให้ Apple ตรวจสอบ คุณต้องมีบัญชีนักพัฒนาแอปจึงจะดำเนินการนี้ได้ หากยังไม่มีบัญชี ให้ลงทะเบียนเข้าร่วมโปรแกรมนักพัฒนาแอป Apple
ข้อตกลงสำหรับแอปที่ต้องซื้อ
หากต้องการใช้การซื้อในแอป คุณต้องมีข้อตกลงที่ใช้งานอยู่สำหรับแอปที่ต้องซื้อใน App Store Connect ด้วย ไปที่ https://appstoreconnect.apple.com/ แล้วคลิกข้อตกลง ภาษี และการธนาคาร
คุณจะเห็นข้อตกลงสำหรับแอปแบบไม่มีค่าใช้จ่ายและแอปแบบชำระเงินที่นี่ สถานะของแอปที่ไม่มีค่าใช้จ่ายควรเป็น "ใช้งานอยู่" และสถานะของแอปที่ต้องซื้อควรเป็น "ใหม่" โปรดอ่านข้อกำหนด ยอมรับข้อกำหนด และป้อนข้อมูลที่จำเป็นทั้งหมด
เมื่อตั้งค่าทุกอย่างถูกต้องแล้ว สถานะสำหรับแอปที่ต้องซื้อจะเปิดใช้งาน ขั้นตอนนี้สำคัญมากเนื่องจากคุณจะลองซื้อในแอปไม่ได้หากไม่มีข้อตกลงที่ใช้งานอยู่
ลงทะเบียนรหัสแอป
สร้างตัวระบุใหม่ในพอร์ทัลนักพัฒนาแอปของ Apple
เลือกรหัสแอป
เลือกแอป
ระบุคำอธิบายและตั้งค่ารหัสกลุ่มให้ตรงกับรหัสกลุ่มที่มีค่าเดียวกันกับที่ตั้งไว้ใน XCode ก่อนหน้านี้
ดูคําแนะนําเพิ่มเติมเกี่ยวกับวิธีสร้างรหัสแอปใหม่ได้ที่ความช่วยเหลือเกี่ยวกับบัญชีนักพัฒนาแอป
การสร้างแอปใหม่
สร้างแอปใหม่ใน App Store Connect ด้วยตัวระบุแพ็กเกจที่ไม่ซ้ำกัน
ดูคำแนะนำเพิ่มเติมเกี่ยวกับวิธีสร้างแอปใหม่และจัดการข้อตกลงได้ที่ความช่วยเหลือเกี่ยวกับ App Store Connect
หากต้องการทดสอบการซื้อในแอป คุณต้องมีผู้ใช้ทดสอบในแซนด์บ็อกซ์ ผู้ใช้ทดสอบนี้ไม่ควรเชื่อมต่อกับ iTunes เนื่องจากใช้เพื่อทดสอบการซื้อในแอปเท่านั้น คุณใช้อีเมลที่มีการใช้งานสำหรับบัญชี Apple อยู่แล้วไม่ได้ ในส่วนผู้ใช้และการเข้าถึง ให้ไปที่ผู้ทดสอบในส่วนแซนด์บ็อกซ์เพื่อสร้างบัญชีแซนด์บ็อกซ์ใหม่หรือจัดการ Apple ID ของแซนด์บ็อกซ์ที่มีอยู่
ตอนนี้คุณตั้งค่าผู้ใช้แซนด์บ็อกซ์ใน iPhone ได้แล้วโดยไปที่การตั้งค่า > App Store > บัญชีแซนด์บ็อกซ์
การกำหนดค่าการซื้อในแอป
ตอนนี้คุณจะต้องกำหนดค่าไอเทมที่ซื้อได้ 3 รายการ ดังนี้
dash_consumable_2k
: การซื้อที่ใช้แล้วหมดไปซึ่งซื้อซ้ำได้หลายครั้ง โดยให้ Dashes (สกุลเงินในแอป) แก่ผู้ใช้ 2, 000 รายการต่อการซื้อ 1 ครั้งdash_upgrade_3d
: การซื้อ "การอัปเกรด" แบบใช้ไม่ได้ซึ่งซื้อได้เพียงครั้งเดียว และทำให้ Dash to Click ของผู้ใช้มีรูปลักษณ์ที่แตกต่างออกไปdash_subscription_doubler
: การสมัครใช้บริการที่ให้ผู้ใช้ได้รับ Dash เพิ่มขึ้น 2 เท่าต่อการคลิก 1 ครั้งตลอดระยะเวลาการสมัครใช้บริการ
ไปที่การซื้อในแอป > จัดการ
สร้างการซื้อในแอปด้วยรหัสที่ระบุไว้ดังต่อไปนี้
- ตั้งค่า
dash_consumable_2k
เป็นไอเทมที่บริโภคได้
ใช้ dash_consumable_2k
เป็นรหัสผลิตภัณฑ์ ชื่ออ้างอิงจะใช้ใน App Store Connect เท่านั้น เพียงตั้งค่าเป็น dash consumable 2k
แล้วเพิ่มการแปลภาษาสำหรับการซื้อ เรียกการซื้อว่า Spring is in the air
โดยมีคำอธิบายเป็น 2000 dashes fly out
- ตั้งค่า
dash_upgrade_3d
เป็นสินค้าที่ไม่บริโภคได้
ใช้ dash_upgrade_3d
เป็นรหัสผลิตภัณฑ์ ตั้งชื่ออ้างอิงเป็น dash upgrade 3d
แล้วเพิ่มการแปลสำหรับการซื้อ เรียกการซื้อว่า 3D Dash
โดยมีคำอธิบายเป็น Brings your dash back to the future
- ตั้งค่า
dash_subscription_doubler
เป็นการสมัครใช้บริการแบบต่ออายุใหม่อัตโนมัติ
ขั้นตอนการสมัครใช้บริการจะแตกต่างออกไปเล็กน้อย ก่อนอื่นคุณต้องตั้งชื่ออ้างอิงและรหัสผลิตภัณฑ์ โดยทำดังนี้
ถัดไป คุณต้องสร้างกลุ่มการสมัครใช้บริการ เมื่อการสมัครใช้บริการหลายรายการอยู่ในกลุ่มเดียวกัน ผู้ใช้จะสมัครใช้บริการรายการใดรายการหนึ่งได้พร้อมกันเท่านั้น แต่สามารถอัปเกรดหรือดาวน์เกรดการสมัครใช้บริการเหล่านี้ได้อย่างง่ายดาย เพียงตั้งชื่อกลุ่มนี้ว่า subscriptions
จากนั้นป้อนระยะเวลาการสมัครใช้บริการและการแปล ตั้งชื่อการสมัครใช้บริการนี้ว่า Jet Engine
พร้อมคำอธิบาย Doubles your clicks
คลิกบันทึก
หลังจากคลิกปุ่มบันทึกแล้ว ให้เพิ่มราคาการสมัครใช้บริการ เลือกราคาที่ต้องการ
ตอนนี้คุณควรเห็นรายการการซื้อ 3 รายการในรายการการซื้อ ดังนี้
5. ตั้งค่า Play Store
คุณจะต้องมีบัญชีนักพัฒนาแอปสำหรับ Play Store ด้วย เช่นเดียวกับ App Store หากยังไม่มีบัญชี ให้ลงทะเบียนบัญชี
สร้างแอปใหม่
สร้างแอปใหม่ใน Google Play Console โดยทำดังนี้
- เปิด Play Console
- เลือกแอปทั้งหมด > สร้างแอป
- เลือกภาษาเริ่มต้นแล้วเพิ่มชื่อแอป พิมพ์ชื่อแอปตามที่ต้องการให้ปรากฏใน Google Play คุณเปลี่ยนชื่อได้ในภายหลัง
- ระบุว่าแอปพลิเคชันของคุณเป็นเกม คุณเปลี่ยนข้อมูลนี้ได้ในภายหลัง
- ระบุว่าแอปพลิเคชันของคุณเป็นแบบฟรีหรือต้องซื้อ
- เพิ่มอีเมลที่ผู้ใช้ Play Store จะใช้เพื่อติดต่อคุณเกี่ยวกับแอปพลิเคชันนี้ได้
- ปฏิบัติตามประกาศหลักเกณฑ์ด้านเนื้อหาและกฎหมายการส่งออกของสหรัฐอเมริกาให้ครบถ้วน
- เลือกสร้างแอป
หลังจากสร้างแอปแล้ว ให้ไปที่แดชบอร์ด แล้วทํางานทั้งหมดในส่วนตั้งค่าแอปให้เสร็จ ในส่วนนี้ คุณต้องระบุข้อมูลบางอย่างเกี่ยวกับแอป เช่น การจัดประเภทเนื้อหาและภาพหน้าจอ
ลงนามในใบสมัคร
คุณต้องอัปโหลดบิลด์อย่างน้อย 1 รายการไปยัง Google Play จึงจะทดสอบการซื้อในแอปได้
ในกรณีนี้ คุณจะต้องรับรองบิลด์รุ่นด้วยสิ่งอื่นที่ไม่ใช่คีย์แก้ไขข้อบกพร่อง
สร้างคีย์สโตร์
หากมีที่เก็บคีย์อยู่แล้ว ให้ข้ามไปยังขั้นตอนถัดไป หากไม่มี ให้สร้างโดยเรียกใช้คำสั่งต่อไปนี้ในบรรทัดคำสั่ง
ใน Mac/Linux ให้ใช้คำสั่งต่อไปนี้
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
ใน Windows ให้ใช้คำสั่งต่อไปนี้
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
คำสั่งนี้จะจัดเก็บไฟล์ key.jks
ในไดเรกทอรีหน้าแรก หากต้องการจัดเก็บไฟล์ไว้ที่อื่น ให้เปลี่ยนอาร์กิวเมนต์ที่คุณส่งไปยังพารามิเตอร์ -keystore
Keep the
keystore
ไฟล์ส่วนตัว อย่าตรวจสอบในระบบควบคุมแหล่งที่มาแบบสาธารณะ
อ้างอิงคีย์สโตร์จากแอป
สร้างไฟล์ชื่อ <your app dir>/android/key.properties
ที่มีข้อมูลอ้างอิงถึงคีย์สโตร์ โดยทำดังนี้
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
กำหนดค่าการรับรองใน Gradle
กำหนดค่าการรับรองแอปโดยแก้ไขไฟล์ <your app dir>/android/app/build.gradle
เพิ่มข้อมูลคีย์สโตร์จากไฟล์พร็อพเพอร์ตี้ก่อนบล็อก android
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
โหลดไฟล์ key.properties
ลงในออบเจ็กต์ keystoreProperties
เพิ่มโค้ดต่อไปนี้ก่อนบล็อก buildTypes
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
กำหนดค่าบล็อก signingConfigs
ในไฟล์ build.gradle
ของโมดูลด้วยข้อมูลการกำหนดค่าการรับรอง โดยทำดังนี้
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
ตอนนี้ระบบจะเซ็นชื่อรุ่นที่เผยแพร่ของแอปโดยอัตโนมัติ
ดูข้อมูลเพิ่มเติมเกี่ยวกับการรับรองแอปได้ที่รับรองแอปใน developer.android.com
อัปโหลดบิลด์แรก
หลังจากกำหนดค่าแอปสำหรับการลงนามแล้ว คุณควรสร้างแอปพลิเคชันได้โดยเรียกใช้คำสั่งต่อไปนี้
flutter build appbundle
คำสั่งนี้จะสร้างบิลด์รุ่นโดยค่าเริ่มต้น และคุณดูเอาต์พุตได้ที่ <your app dir>/build/app/outputs/bundle/release/
จากหน้าแดชบอร์ดใน Google Play Console ให้ไปที่รุ่น > การทดสอบ > การทดสอบแบบปิด แล้วสร้างรุ่นการทดสอบแบบปิดใหม่
สําหรับโค้ดแล็บนี้ คุณจะใช้ Google เป็นผู้ลงนามในแอป ดังนั้นให้กดต่อไปในส่วน Play App Signing เพื่อเลือกใช้
ถัดไป ให้อัปโหลด App Bundle app-release.aab
ที่สร้างขึ้นโดยคําสั่ง build
คลิกบันทึก แล้วคลิกตรวจสอบรุ่น
สุดท้าย ให้คลิกเริ่มการเปิดตัวในการทดสอบภายในเพื่อเปิดใช้งานรุ่นการทดสอบภายใน
ตั้งค่าผู้ใช้ทดสอบ
หากต้องการทดสอบการซื้อในแอป คุณต้องเพิ่มบัญชี Google ของผู้ทดสอบใน Google Play Console 2 ตำแหน่งดังนี้
- ไปยังแทร็กทดสอบที่เฉพาะเจาะจง (การทดสอบภายใน)
- ในฐานะผู้ทดสอบที่มีใบอนุญาต
ก่อนอื่น ให้เริ่มด้วยการเพิ่มผู้ทดสอบลงในแทร็กทดสอบภายใน กลับไปที่รุ่น > การทดสอบ > การทดสอบภายใน แล้วคลิกแท็บผู้ทดสอบ
สร้างรายชื่ออีเมลใหม่โดยคลิกสร้างรายชื่ออีเมล ตั้งชื่อรายการ แล้วเพิ่มอีเมลของบัญชี Google ที่ต้องการเข้าถึงการทดสอบการซื้อในแอป
จากนั้นเลือกช่องทําเครื่องหมายของรายการ แล้วคลิกบันทึกการเปลี่ยนแปลง
จากนั้นเพิ่มผู้ทดสอบที่มีใบอนุญาตโดยทำดังนี้
- กลับไปที่มุมมองแอปทั้งหมดของ Google Play Console
- ไปที่การตั้งค่า > การทดสอบใบอนุญาต
- เพิ่มอีเมลเดียวกันของผู้ทดสอบที่จำเป็นต้องทดสอบการซื้อในแอป
- ตั้งค่าการตอบกลับใบอนุญาตเป็น
RESPOND_NORMALLY
- คลิกบันทึกการเปลี่ยนแปลง
การกำหนดค่าการซื้อในแอป
ตอนนี้คุณจะต้องกำหนดค่าไอเทมที่ซื้อภายในแอปได้
คุณต้องกำหนดการซื้อ 3 รายการที่แตกต่างกัน เช่นเดียวกับใน App Store ดังนี้
dash_consumable_2k
: การซื้อที่ใช้แล้วหมดไปซึ่งซื้อซ้ำได้หลายครั้ง โดยให้ Dashes (สกุลเงินในแอป) แก่ผู้ใช้ 2, 000 รายการต่อการซื้อ 1 ครั้งdash_upgrade_3d
: การซื้อ "การอัปเกรด" แบบใช้ไม่ได้ซึ่งซื้อได้เพียงครั้งเดียว ซึ่งจะทำให้ Dash to Click ของผู้ใช้มีรูปลักษณ์ที่แตกต่างออกไปdash_subscription_doubler
: การสมัครใช้บริการที่ให้ผู้ใช้ได้รับ Dash เพิ่มขึ้น 2 เท่าต่อการคลิก 1 ครั้งตลอดระยะเวลาการสมัครใช้บริการ
ก่อนอื่นให้เพิ่มไอเทมที่บริโภคได้และบริโภคไม่ได้
- ไปที่ Google Play Console แล้วเลือกแอปพลิเคชัน
- ไปที่สร้างรายได้ > ผลิตภัณฑ์ > ไอเทมที่ซื้อในแอป
- คลิกสร้างผลิตภัณฑ์
- ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับผลิตภัณฑ์ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณตั้งใจจะใช้ทุกประการ
- คลิกบันทึก
- คลิกเปิดใช้งาน
- ทำขั้นตอนเดิมซ้ำสำหรับการซื้อ "การอัปเกรด" แบบใช้ไม่ได้
จากนั้นเพิ่มการสมัครใช้บริการโดยทำดังนี้
- ไปที่ Google Play Console แล้วเลือกแอปพลิเคชัน
- ไปที่สร้างรายได้ > ผลิตภัณฑ์ > การสมัครใช้บริการ
- คลิกสร้างการสมัครใช้บริการ
- ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับการสมัครใช้บริการ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณต้องการใช้ทุกประการ
- คลิกบันทึก
ตอนนี้คุณควรตั้งค่าการซื้อใน Play Console แล้ว
6. ตั้งค่า Firebase
ในโค้ดแล็บนี้ คุณจะใช้บริการแบ็กเอนด์เพื่อยืนยันและติดตามการซื้อของผู้ใช้
การใช้บริการแบ็กเอนด์มีประโยชน์หลายประการ ดังนี้
- คุณสามารถยืนยันธุรกรรมได้อย่างปลอดภัย
- คุณสามารถดำเนินการกับเหตุการณ์การเรียกเก็บเงินจาก App Store ได้
- คุณสามารถติดตามการซื้อในฐานข้อมูลได้
- ผู้ใช้จะหลอกแอปให้แสดงฟีเจอร์พรีเมียมโดยการกรอนาฬิการะบบย้อนกลับไม่ได้
แม้ว่าจะมีวิธีตั้งค่าบริการแบ็กเอนด์หลายวิธี แต่คุณจะทําได้โดยใช้ฟังก์ชันระบบคลาวด์และ 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
เป็นไปได้ว่าคีย์สโตร์จะอยู่ในโฟลเดอร์บ้านในส่วน .android/debug.keystore
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
คัดลอกแฮช SHA-1 แล้วกรอกข้อมูลในช่องสุดท้ายในกล่องโต้ตอบแบบโมดัลการส่งแอป
ตั้งค่า Firebase สําหรับ iOS: ขั้นตอนเพิ่มเติม
เปิด ios/Runnder.xcworkspace
ด้วย Xcode
หรือจะใช้ IDE ที่คุณเลือกก็ได้
ใน VSCode ให้คลิกขวาที่โฟลเดอร์ ios/
แล้วคลิก open in xcode
ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ ios/
แล้วคลิก flutter
ตามด้วยตัวเลือก open iOS module in Xcode
หากต้องการอนุญาตให้ใช้ Google Sign-In ใน iOS ให้เพิ่มตัวเลือกการกําหนดค่า CFBundleURLTypes
ลงในไฟล์ plist
ของบิลด์ (ดูข้อมูลเพิ่มเติมในเอกสารของแพ็กเกจ google_sign_in
) ในกรณีนี้ ไฟล์คือ ios/Runner/Info-Debug.plist
และ ios/Runner/Info-Release.plist
มีการเพิ่มคู่คีย์-ค่าแล้ว แต่ต้องแทนที่ค่าด้วยค่าต่อไปนี้
- รับค่าของ
REVERSED_CLIENT_ID
จากไฟล์GoogleService-Info.plist
โดยไม่มีองค์ประกอบ<string>..</string>
ล้อมรอบ - แทนที่ค่าทั้งในไฟล์
ios/Runner/Info-Debug.plist
และios/Runner/Info-Release.plist
ภายใต้คีย์CFBundleURLTypes
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
เท่านี้ก็เสร็จสิ้นการตั้งค่า Firebase แล้ว
7. ฟังข้อมูลอัปเดตเกี่ยวกับการซื้อ
ในส่วนนี้ของ Codelab คุณจะต้องเตรียมแอปสำหรับการซื้อผลิตภัณฑ์ กระบวนการนี้รวมถึงการฟังการอัปเดตการซื้อและข้อผิดพลาดหลังจากที่แอปเริ่มทำงาน
ฟังการอัปเดตการซื้อ
ใน main.dart,
ให้ค้นหาวิดเจ็ต MyHomePage
ที่มี Scaffold
ที่มี BottomNavigationBar
ซึ่งมี 2 หน้า หน้านี้ยังสร้าง Provider
3 รายการสําหรับ DashCounter
, DashUpgrades,
และ DashPurchases
ด้วย DashCounter
จะติดตามจำนวนขีดกลางปัจจุบันและเพิ่มขีดกลางโดยอัตโนมัติ DashUpgrades
จะจัดการการอัปเกรดที่คุณซื้อได้ด้วย Dash Codelab นี้มุ่งเน้นที่ DashPurchases
โดยค่าเริ่มต้น ระบบจะกำหนดออบเจ็กต์ของผู้ให้บริการเมื่อมีการขอออบเจ็กต์นั้นเป็นครั้งแรก ออบเจ็กต์นี้จะคอยฟังการอัปเดตการซื้อโดยตรงเมื่อแอปเริ่มทำงาน ดังนั้นให้ปิดใช้การโหลดแบบเลื่อนเวลาในออบเจ็กต์นี้ด้วย lazy: false
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
นอกจากนี้ คุณจะต้องมีอินสแตนซ์ของ InAppPurchaseConnection
ด้วย อย่างไรก็ตาม คุณต้องมีวิธีจำลองการเชื่อมต่อเพื่อให้แอปทดสอบได้ โดยสร้างเมธอดอินสแตนซ์ที่ลบล้างได้ในการทดสอบ แล้วเพิ่มลงใน main.dart
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
คุณต้องอัปเดตการทดสอบเล็กน้อยหากต้องการให้การทดสอบทำงานต่อไป
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable(
{required PurchaseParam purchaseParam, bool autoConsume = true}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(ProductDetailsResponse(
productDetails: [],
notFoundIDs: [],
));
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
ใน lib/logic/dash_purchases.dart
ให้ไปที่รหัสสำหรับ DashPurchases ChangeNotifier
ปัจจุบันมีเพียง DashCounter
เท่านั้นที่คุณเพิ่มลงใน Dash ที่ซื้อได้
เพิ่มพร็อพเพอร์ตี้การสมัครใช้บริการสตรีม _subscription
(ประเภท StreamSubscription<List<PurchaseDetails>> _subscription;
) IAPConnection.instance,
และการนําเข้า โค้ดที่ได้ควรมีลักษณะดังนี้
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
ระบบจะเพิ่มคีย์เวิร์ด late
ลงใน _subscription
เนื่องจากมีการเริ่มต้น _subscription
ในเครื่องมือสร้าง โปรเจ็กต์นี้ได้รับการตั้งค่าให้ไม่ใช่ Null โดยค่าเริ่มต้น (NNBD) ซึ่งหมายความว่าพร็อพเพอร์ตี้ที่ไม่ได้ประกาศว่าอนุญาตค่า Null ต้องมีค่าที่ไม่ใช่ Null ตัวคําจํากัด late
ช่วยให้คุณเลื่อนการกําหนดค่านี้ได้
ในคอนสตรัคเตอร์ ให้รับสตรีม purchaseUpdated
และเริ่มฟังสตรีม ในวิธีการ dispose()
ให้ยกเลิกการสมัครใช้บริการสตรีม
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
ตอนนี้แอปได้รับการอัปเดตการซื้อแล้ว คุณจึงจะทำการซื้อได้ในส่วนถัดไป
ก่อนดำเนินการต่อ ให้ทำการทดสอบด้วย "flutter test"
เพื่อยืนยันว่าทุกอย่างตั้งค่าอย่างถูกต้องแล้ว
$ flutter test
00:01 +1: All tests passed!
8. ซื้อสินค้าหรือบริการ
ในส่วนนี้ของโค้ดแล็บ คุณจะต้องแทนที่ผลิตภัณฑ์จำลองที่มีอยู่ด้วยผลิตภัณฑ์จริงที่ซื้อได้ ระบบจะโหลดผลิตภัณฑ์เหล่านี้จากร้านค้า แสดงเป็นรายการ และทำการสั่งซื้อเมื่อแตะผลิตภัณฑ์
ปรับ PurchasableProduct
PurchasableProduct
แสดงผลิตภัณฑ์จำลอง อัปเดตให้แสดงเนื้อหาจริงโดยแทนที่คลาส PurchasableProduct
ใน purchasable_product.dart
ด้วยโค้ดต่อไปนี้
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
ใน dash_purchases.dart,
ให้นําการซื้อจำลองออกและแทนที่ด้วยรายการว่าง List<PurchasableProduct> products = [];
โหลดรายการที่ซื้อได้
หากต้องการให้ผู้ใช้ทำการซื้อ ให้โหลดรายการซื้อจากร้านค้า ก่อนอื่น ให้ตรวจสอบว่าร้านค้าพร้อมให้บริการหรือไม่ เมื่อร้านค้าไม่พร้อมใช้งาน การตั้งค่า storeState
เป็น notAvailable
จะแสดงข้อความแสดงข้อผิดพลาดแก่ผู้ใช้
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
เมื่อร้านค้าพร้อมใช้งาน ให้โหลดรายการที่ซื้อได้ คุณควรเห็น storeKeyConsumable
, storeKeySubscription,
และ storeKeyUpgrade
จากการตั้งค่า Firebase ก่อนหน้านี้ เมื่อการซื้อที่คาดไว้ไม่พร้อมใช้งาน ให้พิมพ์ข้อมูลนี้ลงในคอนโซล นอกจากนี้ คุณอาจต้องส่งข้อมูลนี้ไปยังบริการแบ็กเอนด์ด้วย
เมธอด await iapConnection.queryProductDetails(ids)
จะแสดงทั้งรหัสที่ระบบไม่พบและผลิตภัณฑ์ที่ซื้อได้ซึ่งระบบพบ ใช้ productDetails
จากคำตอบเพื่ออัปเดต UI และตั้งค่า StoreState
เป็น available
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
เรียกฟังก์ชัน loadPurchases()
ในเครื่องมือสร้าง
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
สุดท้าย ให้เปลี่ยนค่าของช่อง storeState
จาก StoreState.available
เป็น StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
แสดงผลิตภัณฑ์ที่ซื้อได้
พิจารณาไฟล์ purchase_page.dart
วิดเจ็ต PurchasePage
จะแสดง _PurchasesLoading
, _PurchaseList,
หรือ _PurchasesNotAvailable,
โดยขึ้นอยู่กับ StoreState
วิดเจ็ตยังแสดงรายการที่ผู้ใช้ซื้อก่อนหน้านี้ซึ่งจะใช้ในขั้นตอนถัดไปด้วย
วิดเจ็ต _PurchaseList
จะแสดงรายการผลิตภัณฑ์ที่ซื้อได้และส่งคำขอซื้อไปยังออบเจ็กต์ DashPurchases
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
คุณควรเห็นผลิตภัณฑ์ที่พร้อมจำหน่ายใน Store ของ Android และ iOS หากมีการกําหนดค่าอย่างถูกต้อง โปรดทราบว่าระบบอาจใช้เวลาสักครู่ก่อนที่การซื้อจะพร้อมใช้งานเมื่อป้อนลงในคอนโซลที่เกี่ยวข้อง
กลับไปที่ 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) กับเครื่องที่เซิร์ฟเวอร์จะทำงานได้ โดยอุปกรณ์ต้องอยู่ในเครือข่ายเดียวกัน และคุณจำเป็นต้องทราบที่อยู่ 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 Console แล้วเริ่มจากหน้าแอปทั้งหมด
- ไปที่การตั้งค่า > การเข้าถึง API
ในกรณีที่ Google Play Console ขอให้คุณสร้างหรือลิงก์กับโปรเจ็กต์ที่มีอยู่ ให้ดำเนินการดังกล่าวก่อนแล้วค่อยกลับมาที่หน้านี้
- ค้นหาส่วนที่คุณกำหนดบัญชีบริการได้ แล้วคลิกสร้างบัญชีบริการใหม่
- คลิกลิงก์ Google Cloud Platform ในกล่องโต้ตอบที่ปรากฏขึ้น
- เลือกโปรเจ็กต์ หากไม่เห็น ให้ตรวจสอบว่าคุณได้ลงชื่อเข้าใช้บัญชี Google ที่ถูกต้องแล้วในรายการแบบเลื่อนลงบัญชีที่ด้านขวาบน
- หลังจากเลือกโปรเจ็กต์แล้ว ให้คลิก + สร้างบัญชีบริการในแถบเมนูด้านบน
- ระบุชื่อบัญชีบริการ ระบุคำอธิบาย (ไม่บังคับ) เพื่อให้จำวัตถุประสงค์ของบัญชีได้ แล้วไปยังขั้นตอนถัดไป
- มอบหมายบทบาทผู้แก้ไขให้กับบัญชีบริการ
- ทําตามวิซาร์ดให้เสร็จสิ้น แล้วกลับไปที่หน้าการเข้าถึง API ในคอนโซลนักพัฒนาแอป แล้วคลิกรีเฟรชบัญชีบริการ คุณควรเห็นบัญชีที่สร้างใหม่ในรายการ
- คลิกให้สิทธิ์เข้าถึงสําหรับบัญชีบริการใหม่
- เลื่อนหน้าถัดไปลงไปที่บล็อกข้อมูลทางการเงิน เลือกทั้งดูข้อมูลทางการเงิน คำสั่งซื้อ และการตอบแบบสำรวจการยกเลิกและจัดการคำสั่งซื้อและการสมัครใช้บริการ
- คลิกเชิญผู้ใช้
- เมื่อตั้งค่าบัญชีแล้ว คุณก็แค่สร้างข้อมูลเข้าสู่ระบบ กลับไปที่คอนโซลระบบคลาวด์ แล้วค้นหาบัญชีบริการในรายการบัญชีบริการ คลิกจุดแนวตั้ง 3 จุด แล้วเลือกจัดการคีย์
- สร้างคีย์ JSON ใหม่และดาวน์โหลด
- เปลี่ยนชื่อไฟล์ที่ดาวน์โหลดเป็น
service-account-google-play.json,
แล้วย้ายไปไว้ในไดเรกทอรีassets/
อีกอย่างหนึ่งที่เราต้องทำคือเปิด 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 ร้านค้า แอปพลิเคชันของคุณจะได้รับโทเค็นเมื่อมีการซื้อ
แอปจะส่งโทเค็นนี้ไปยังบริการแบ็กเอนด์ของคุณ ซึ่งจะยืนยันการซื้อกับเซิร์ฟเวอร์ของร้านค้าที่เกี่ยวข้องโดยใช้โทเค็นที่ระบุ
จากนั้นบริการแบ็กเอนด์จะเลือกจัดเก็บการซื้อและตอบกลับแอปพลิเคชันว่าการซื้อนั้นถูกต้องหรือไม่
การมีบริการแบ็กเอนด์ตรวจสอบกับ Store แทนแอปพลิเคชันที่ทำงานในอุปกรณ์ของผู้ใช้จะช่วยป้องกันไม่ให้ผู้ใช้เข้าถึงฟีเจอร์พรีเมียมได้ เช่น การกรอนาฬิการะบบย้อนกลับ
ตั้งค่าฝั่ง Flutter
ตั้งค่าการตรวจสอบสิทธิ์
เนื่องจากคุณกำลังจะส่งการซื้อไปยังบริการแบ็กเอนด์ คุณจึงต้องตรวจสอบว่าผู้ใช้ได้รับการตรวจสอบสิทธิ์ขณะทำการซื้อ เราได้เพิ่มตรรกะการตรวจสอบสิทธิ์ส่วนใหญ่ไว้ในโปรเจ็กต์เริ่มต้นแล้ว คุณเพียงแค่ต้องตรวจสอบว่า PurchasePage
แสดงปุ่มเข้าสู่ระบบเมื่อผู้ใช้ยังไม่ได้เข้าสู่ระบบ เพิ่มโค้ดต่อไปนี้ไว้ที่ตอนต้นของเมธอด build ของ PurchasePage
lib/pages/purchase_page.dart
import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
โทรหาปลายทางการยืนยันจากแอป
ในแอป ให้สร้างฟังก์ชัน _verifyPurchase(PurchaseDetails purchaseDetails)
ที่เรียกใช้ปลายทาง /verifypurchase
ในแบ็กเอนด์ Dart โดยใช้การเรียก HTTP POST
ส่งร้านค้าที่เลือก (google_play
สำหรับ Play Store หรือ app_store
สำหรับ App Store), serverVerificationData
และ productID
เซิร์ฟเวอร์จะแสดงรหัสสถานะที่ระบุว่าการซื้อได้รับการยืนยันหรือไม่
ในค่าคงที่ของแอป ให้กําหนดค่า IP ของเซิร์ฟเวอร์เป็นที่อยู่ IP ของเครื่อง
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
เพิ่ม firebaseNotifier
กับการสร้าง DashPurchases
ใน main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
เพิ่ม getter สําหรับผู้ใช้ใน FirebaseNotifier เพื่อให้ส่งรหัสผู้ใช้ไปยังฟังก์ชันยืนยันการซื้อได้
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
เพิ่มฟังก์ชัน _verifyPurchase
ลงในคลาส DashPurchases
ฟังก์ชัน async
นี้จะแสดงผลบูลีนซึ่งระบุว่าการซื้อได้รับการตรวจสอบหรือไม่
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
เรียกใช้ฟังก์ชัน _verifyPurchase
ใน _handlePurchase
ก่อนที่คุณจะใช้การซื้อ คุณควรใช้การซื้อเมื่อได้รับการยืนยันแล้วเท่านั้น ในแอปเวอร์ชันที่ใช้งานจริง คุณสามารถระบุค่านี้เพิ่มเติมได้ เช่น ใช้การสมัครใช้บริการช่วงทดลองใช้เมื่อ Store ไม่พร้อมให้บริการชั่วคราว อย่างไรก็ตาม ในตัวอย่างนี้ เราจะใช้วิธีการที่ง่ายที่สุด และจะใช้การซื้อก็ต่อเมื่อการซื้อได้รับการยืนยันเรียบร้อยแล้วเท่านั้น
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(1000);
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
ตอนนี้ทุกอย่างในแอปพร้อมตรวจสอบการซื้อแล้ว
ตั้งค่าบริการแบ็กเอนด์
ถัดไป ให้ตั้งค่า Cloud Function เพื่อยืนยันการซื้อในแบ็กเอนด์
สร้างตัวแฮนเดิลการซื้อ
เนื่องจากขั้นตอนการยืนยันของทั้ง 2 ร้านค้าเกือบจะเหมือนกัน ให้ตั้งค่าคลาส PurchaseHandler
นามธรรมที่มีการติดตั้งใช้งานแยกกันสำหรับแต่ละร้านค้า
เริ่มต้นด้วยการเพิ่มไฟล์ purchase_handler.dart
ลงในโฟลเดอร์ lib/
ซึ่งคุณกำหนดคลาส PurchaseHandler
นามธรรมที่มีเมธอดนามธรรม 2 รายการสำหรับการยืนยันการซื้อ 2 ประเภท ได้แก่ การสมัครใช้บริการและการไม่สมัครใช้บริการ
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
ดังที่คุณเห็น แต่ละเมธอดต้องใช้พารามิเตอร์ 3 รายการ ได้แก่
userId:
รหัสของผู้ใช้ที่เข้าสู่ระบบเพื่อให้คุณเชื่อมโยงการซื้อกับผู้ใช้ได้productData:
ข้อมูลเกี่ยวกับผลิตภัณฑ์ คุณจะต้องกําหนดค่านี้ในอีกสักครู่token:
โทเค็นที่ร้านค้ามอบให้แก่ผู้ใช้
นอกจากนี้ หากต้องการให้ตัวแฮนเดิลการซื้อเหล่านี้ใช้งานได้ง่ายขึ้น ให้เพิ่มเมธอด verifyPurchase()
ที่ใช้ได้ทั้งสำหรับการสมัครใช้บริการและไม่สมัครใช้บริการ ดังนี้
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
ตอนนี้คุณเรียก verifyPurchase
สำหรับทั้ง 2 กรณีได้ แต่ยังคงมีการใช้งานแยกกัน
คลาส ProductData
มีข้อมูลพื้นฐานเกี่ยวกับผลิตภัณฑ์ต่างๆ ที่ซื้อได้ ซึ่งรวมถึงรหัสผลิตภัณฑ์ (บางครั้งเรียกว่า SKU) และ ProductType
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ProductType
อาจเป็นแบบสมัครใช้บริการหรือไม่สมัครใช้บริการก็ได้
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
สุดท้าย ระบบจะกำหนดรายการผลิตภัณฑ์เป็นแผนที่ในไฟล์เดียวกัน
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
ถัดไป ให้กำหนดการใช้งานตัวยึดตำแหน่งบางอย่างสำหรับ Google Play Store และ Apple App Store เริ่มต้นใช้งาน Google Play
สร้าง lib/google_play_purchase_handler.dart
และเพิ่มคลาสที่ขยาย PurchaseHandler
ที่คุณเพิ่งเขียน
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
ขณะนี้จะแสดงผล true
สำหรับเมธอดแฮนเดิล ซึ่งคุณจะดูได้ภายหลัง
อย่างที่คุณอาจสังเกตเห็น ตัวสร้างใช้อินสแตนซ์ของ IapRepository
แฮนเดิลการซื้อใช้อินสแตนซ์นี้เพื่อจัดเก็บข้อมูลเกี่ยวกับการซื้อใน Firestore ในภายหลัง หากต้องการสื่อสารกับ Google Play คุณจะใช้ AndroidPublisherApi
ที่ระบุไว้
จากนั้นทําเช่นเดียวกันกับตัวแฮนเดิล App Store สร้าง lib/app_store_purchase_handler.dart
แล้วเพิ่มชั้นเรียนที่ขยาย PurchaseHandler
อีกครั้ง โดยทำดังนี้
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
เยี่ยม! ตอนนี้คุณมีตัวแฮนเดิลการซื้อ 2 รายการ ต่อไปมาสร้างปลายทาง API การยืนยันการซื้อกัน
ใช้ตัวแฮนเดิลการซื้อ
เปิด bin/server.dart
และสร้างปลายทาง API โดยใช้ shelf_route
โดยทำดังนี้
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
โค้ดด้านบนทําสิ่งต่อไปนี้
- กำหนดปลายทาง 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
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
ยืนยันการซื้อใน Android: ใช้ตัวแฮนเดิลการซื้อ
จากนั้นให้ติดตั้งใช้งานตัวแฮนเดิลการซื้อของ Google Play ต่อ
Google มีแพ็กเกจ Dart สำหรับโต้ตอบกับ API ที่จําเป็นในการยืนยันการซื้ออยู่แล้ว คุณได้เริ่มต้นค่าในไฟล์ server.dart
และตอนนี้ใช้ค่าเหล่านั้นในคลาส GooglePlayPurchaseHandler
ใช้ตัวแฮนเดิลสําหรับการซื้อที่ไม่ใช่ประเภทการสมัครใช้บริการ ดังนี้
lib/google_play_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
คุณอัปเดตตัวแฮนเดิลการซื้อการสมัครใช้บริการได้โดยใช้วิธีคล้ายกัน ดังนี้
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
เพิ่มเมธอดต่อไปนี้เพื่ออำนวยความสะดวกในการแยกวิเคราะห์รหัสคำสั่งซื้อ รวมถึงเมธอด 2 รายการในการแยกวิเคราะห์สถานะการซื้อ
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
ตอนนี้การซื้อใน Google Play ควรได้รับการยืนยันและจัดเก็บไว้ในฐานข้อมูลแล้ว
จากนั้นไปที่การซื้อใน App Store สำหรับ iOS
ยืนยันการซื้อใน iOS: ใช้ตัวแฮนเดิลการซื้อ
สำหรับการยืนยันการซื้อด้วย App Store แพ็กเกจ Dart ของบุคคลที่สามชื่อ app_store_server_sdk
จะช่วยให้กระบวนการนี้ง่ายขึ้น
เริ่มต้นด้วยการสร้างอินสแตนซ์ ITunesApi
ใช้การกําหนดค่าแซนด์บ็อกซ์ รวมถึงเปิดใช้การบันทึกเพื่อให้แก้ไขข้อผิดพลาดได้ง่ายขึ้น
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
ตอนนี้ App Store ใช้ปลายทาง API เดียวกันสำหรับทั้งการสมัครใช้บริการและการไม่สมัครใช้บริการ ซึ่งแตกต่างจาก Google Play API ซึ่งหมายความว่าคุณจะใช้ตรรกะเดียวกันกับตัวแฮนเดิลทั้ง 2 รายการได้ ผสานไฟล์เข้าด้วยกันเพื่อให้เรียกใช้การติดตั้งใช้งานเดียวกัน
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
ตอนนี้ให้ติดตั้ง handleValidation
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
ตอนนี้การซื้อใน App Store ควรได้รับการยืนยันและจัดเก็บไว้ในฐานข้อมูลแล้ว
เรียกใช้แบ็กเอนด์
เมื่อถึงขั้นตอนนี้ คุณสามารถเรียกใช้ dart bin/server.dart
เพื่อแสดงปลายทาง /verifypurchase
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. ติดตามการซื้อ
วิธีที่เราแนะนำในการติดตามการซื้อของผู้ใช้คือในบริการแบ็กเอนด์ เนื่องจากแบ็กเอนด์สามารถตอบสนองต่อเหตุการณ์จากร้านค้าได้ จึงมีแนวโน้มที่จะพบข้อมูลที่ล้าสมัยน้อยกว่าเนื่องจากการแคช และยังมีแนวโน้มที่จะเกิดการแทรกแซงน้อยกว่า
ก่อนอื่น ให้ตั้งค่าการประมวลผลเหตุการณ์ของร้านค้าในแบ็กเอนด์ด้วยแบ็กเอนด์ Dart ที่คุณกำลังสร้าง
ประมวลผลเหตุการณ์ในร้านในแบ็กเอนด์
ร้านค้าสามารถแจ้งข้อมูลเหตุการณ์การเรียกเก็บเงินที่เกิดขึ้นในแบ็กเอนด์ เช่น เมื่อมีการต่ออายุการสมัครใช้บริการ คุณสามารถประมวลผลเหตุการณ์เหล่านี้ในแบ็กเอนด์เพื่อให้การซื้อในฐานข้อมูลเป็นข้อมูลล่าสุดอยู่เสมอ ในส่วนนี้ ให้ตั้งค่าทั้งสำหรับ Google Play Store และ Apple App Store
ประมวลผลเหตุการณ์การเรียกเก็บเงินของ Google Play
Google Play ระบุเหตุการณ์การเรียกเก็บเงินผ่านสิ่งที่เรียกว่าหัวข้อ Cloud Pub/Sub โดยพื้นฐานแล้วคือคิวข้อความที่เผยแพร่ข้อความได้ รวมถึงใช้รับข้อความได้
เนื่องจากเป็นฟังก์ชันการทำงานเฉพาะสำหรับ Google Play คุณจึงต้องใส่ฟังก์ชันการทำงานนี้ไว้ใน GooglePlayPurchaseHandler
เริ่มต้นด้วยการเปิด lib/google_play_purchase_handler.dart
แล้วเพิ่มการนําเข้า PubsubApi โดยทําดังนี้
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
จากนั้นส่ง PubsubApi
ไปยัง GooglePlayPurchaseHandler
และแก้ไขตัวสร้างคลาสเพื่อสร้าง Timer
ดังนี้
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
Timer
ได้รับการกําหนดค่าให้เรียกใช้เมธอด _pullMessageFromSubSub
ทุก 10 วินาที คุณปรับระยะเวลาได้ตามต้องการ
จากนั้นสร้าง _pullMessageFromSubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
โค้ดที่คุณเพิ่งเพิ่มจะสื่อสารกับหัวข้อ Pub/Sub จาก Google Cloud ทุก 10 วินาทีและขอข้อความใหม่ จากนั้นประมวลผลแต่ละข้อความในเมธอด _processMessage
วิธีการนี้จะถอดรหัสข้อความขาเข้าและรับข้อมูลที่อัปเดตเกี่ยวกับการซื้อแต่ละรายการ ทั้งการสมัครใช้บริการและการไม่สมัครใช้บริการ โดยเรียกใช้ handleSubscription
หรือ handleNonSubscription
ที่มีอยู่ หากจำเป็น
แต่ละข้อความต้องได้รับการตอบกลับด้วยเมธอด _askMessage
จากนั้นเพิ่มข้อกำหนดที่จำเป็นลงในไฟล์ server.dart
เพิ่ม PubsubApi.cloudPlatformScope ลงในการกำหนดค่าข้อมูลเข้าสู่ระบบ
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
จากนั้นสร้างอินสแตนซ์ PubsubApi
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
และสุดท้าย ให้ส่งค่าไปยังเครื่องมือสร้าง GooglePlayPurchaseHandler
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
การตั้งค่า Google Play
คุณได้เขียนโค้ดเพื่อใช้เหตุการณ์การเรียกเก็บเงินจากหัวข้อ Pub/Sub แต่ยังไม่ได้สร้างหัวข้อ Pub/Sub หรือเผยแพร่เหตุการณ์การเรียกเก็บเงิน ได้เวลาตั้งค่าแล้ว
ก่อนอื่น ให้สร้างหัวข้อ Pub/Sub โดยทำดังนี้
- ไปที่หน้า Cloud Pub/Sub ในคอนโซล Google Cloud
- ตรวจสอบว่าคุณอยู่ในโปรเจ็กต์ Firebase แล้วคลิก + สร้างหัวข้อ
- ตั้งชื่อหัวข้อใหม่ให้เหมือนกับค่าที่ตั้งไว้สำหรับ
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
ในconstants.ts
ในกรณีนี้ ให้ตั้งชื่อเป็นplay_billing
หากเลือกอย่างอื่น โปรดอัปเดตconstants.ts
สร้างหัวข้อ - ในรายการหัวข้อที่เผยแพร่/ติดตาม ให้คลิกจุดแนวตั้ง 3 จุดของหัวข้อที่คุณเพิ่งสร้าง แล้วคลิกดูสิทธิ์
- ในแถบด้านข้างทางด้านขวา ให้เลือกเพิ่มผู้ใช้หลัก
- เพิ่ม
google-play-developer-notifications@system.gserviceaccount.com
แล้วมอบหมายบทบาทผู้เผยแพร่ Pub/Sub - บันทึกการเปลี่ยนแปลงสิทธิ์
- คัดลอกชื่อหัวข้อของหัวข้อที่คุณเพิ่งสร้างขึ้น
- เปิด Play Console อีกครั้ง แล้วเลือกแอปจากรายการแอปทั้งหมด
- เลื่อนลงแล้วไปที่สร้างรายได้ > การตั้งค่าการสร้างรายได้
- กรอกหัวข้อให้สมบูรณ์และบันทึกการเปลี่ยนแปลง
ระบบจะเผยแพร่เหตุการณ์การเรียกเก็บเงินทั้งหมดของ Google Play ในหัวข้อนี้
ประมวลผลเหตุการณ์การเรียกเก็บเงินของ App Store
จากนั้นทำตามขั้นตอนเดียวกันกับเหตุการณ์การเรียกเก็บเงินของ App Store การจัดการการอัปเดตการซื้อสำหรับ App Store ทำได้ 2 วิธี วิธีหนึ่งคือการใช้ Webhook ที่คุณให้ไว้กับ Apple ซึ่ง Apple จะใช้เพื่อสื่อสารกับเซิร์ฟเวอร์ของคุณ วิธีที่สองซึ่งคุณจะพบในโค้ดแล็บนี้คือการเชื่อมต่อกับ App Store Server API และรับข้อมูลการสมัครใช้บริการด้วยตนเอง
เหตุผลที่โค้ดแล็บนี้มุ่งเน้นที่โซลูชันที่ 2 เนื่องจากคุณจะต้องเปิดเผยเซิร์ฟเวอร์ต่ออินเทอร์เน็ตเพื่อติดตั้งใช้งาน Webhook
ในสภาพแวดล้อมที่ใช้งานจริง คุณควรมีทั้ง 2 รายการ เว็บฮุคเพื่อรับเหตุการณ์จาก App Store และ Server API ในกรณีที่คุณพลาดเหตุการณ์หรือต้องตรวจสอบสถานะการสมัครใช้บริการอีกครั้ง
เริ่มต้นด้วยการเปิด lib/app_store_purchase_handler.dart
แล้วเพิ่ม AppStoreServerAPI ต่อไปนี้
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
แก้ไขคอนสตรัคเตอร์เพื่อเพิ่มตัวจับเวลาที่จะไปเรียกใช้เมธอด _pullStatus
ตัวจับเวลานี้จะเรียกใช้เมธอด _pullStatus
ทุก 10 วินาที คุณสามารถปรับระยะเวลาของนาฬิกาจับเวลานี้ได้ตามต้องการ
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
จากนั้นสร้างเมธอด _pullStatus ดังนี้
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
วิธีการนี้ทํางานดังนี้
- รับรายการการสมัครใช้บริการที่ใช้งานอยู่จาก Firestore โดยใช้ IapRepository
- สำหรับคำสั่งซื้อแต่ละรายการ ระบบจะขอสถานะการสมัครใช้บริการจาก App Store Server API
- รับธุรกรรมล่าสุดสำหรับการซื้อการสมัครใช้บริการนั้น
- ตรวจสอบวันที่หมดอายุ
- อัปเดตสถานะการสมัครใช้บริการใน Firestore หากหมดอายุ ระบบจะทำเครื่องหมายเป็นเช่นนั้น
สุดท้าย ให้เพิ่มโค้ดที่จำเป็นทั้งหมดเพื่อกำหนดค่าการเข้าถึง App Store Server API
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
การตั้งค่า App Store
ขั้นตอนถัดไป ให้ตั้งค่า App Store โดยทำดังนี้
- เข้าสู่ระบบ 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((DocumentSnapshot document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
ตรรกะการซื้อทั้งหมดอยู่ในคลาส DashPurchases
และเป็นตำแหน่งที่ควรใช้หรือนำการสมัครใช้บริการออก ดังนั้น ให้เพิ่ม iapRepo
เป็นพร็อพเพอร์ตี้ในคลาสและกำหนด iapRepo
ในคอนสตรัคเตอร์ จากนั้นเพิ่มตัวรับฟังในคอนสตรคเตอร์โดยตรง และนําตัวรับฟังออกในเมธอด dispose()
ในช่วงแรก Listener อาจเป็นฟังก์ชันว่างก็ได้ เนื่องจาก IAPRepo
เป็น ChangeNotifier
และคุณเรียก notifyListeners()
ทุกครั้งที่มีการเปลี่ยนแปลงการซื้อใน Firestore ระบบจึงเรียกใช้เมธอด purchasesUpdate()
ทุกครั้งที่มีการเปลี่ยนแปลงผลิตภัณฑ์ที่ซื้อ
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
ถัดไป ให้ป้อน IAPRepo
ให้กับคอนสตรคเตอร์ใน main.dart.
คุณสามารถรับที่เก็บข้อมูลได้โดยใช้ context.read
เนื่องจากมีการสร้างที่เก็บข้อมูลใน Provider
อยู่แล้ว
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
ถัดไป ให้เขียนโค้ดสําหรับฟังก์ชัน purchaseUpdate()
ใน dash_counter.dart,
วิธีการ applyPaidMultiplier
และ removePaidMultiplier
จะตั้งค่าตัวคูณเป็น 10 หรือ 1 ตามลำดับ คุณจึงไม่ต้องตรวจสอบว่ามีการสมัครใช้บริการแล้วหรือยัง เมื่อสถานะการสมัครใช้บริการมีการเปลี่ยนแปลง คุณจะต้องอัปเดตสถานะของผลิตภัณฑ์ที่ซื้อได้ด้วยเพื่อให้แสดงในหน้าการซื้อว่าผลิตภัณฑ์ดังกล่าวใช้งานได้แล้ว ตั้งค่าพร็อพเพอร์ตี้ _beautifiedDashUpgrade
โดยพิจารณาว่ามีการซื้อการอัปเกรดหรือไม่
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
ตอนนี้คุณตรวจสอบแล้วว่าสถานะการสมัครใช้บริการและการอัปเกรดเป็นปัจจุบันเสมอในบริการแบ็กเอนด์และซิงค์กับแอป แอปจะดำเนินการตามนั้นและใช้ฟีเจอร์การสมัครใช้บริการและการอัปเกรดกับเกม Dash clicker
12. เสร็จเรียบร้อย
ขอแสดงความยินดี! คุณทำ Codelab เสร็จแล้ว คุณดูโค้ดที่สมบูรณ์สำหรับโค้ดแล็บนี้ได้ในโฟลเดอร์ "สมบูรณ์"
ดูข้อมูลเพิ่มเติมได้ใน Flutter Codelab อื่นๆ