1. บทนำ
อัปเดตล่าสุด 11-07-2023
การเพิ่มการซื้อในแอปลงในแอป Flutter ต้องมีการตั้งค่า App Store และ Play Store ให้ถูกต้อง รวมถึงต้องยืนยันการซื้อและให้สิทธิ์ที่จำเป็น เช่น สิทธิพิเศษในการสมัครใช้บริการ
ใน Codelab นี้ คุณจะต้องเพิ่มการซื้อในแอป 3 ประเภทลงในแอป (จัดไว้ให้) และยืนยันการซื้อเหล่านี้โดยใช้แบ็กเอนด์ของ DART กับ Firebase แอป Dash Clicker ที่จัดเตรียมไว้ให้มีเกมที่ใช้มาสคอต Dash เป็นสกุลเงิน คุณจะเพิ่มตัวเลือกการซื้อต่อไปนี้
- ตัวเลือกการซื้อซ้ำได้ 2,000 เส้นพร้อมกัน
- ซื้อการอัปเกรดครั้งเดียวเพื่อเปลี่ยน Dash สไตล์เก่าให้เป็น Dash สไตล์ทันสมัย
- การสมัครใช้บริการที่เพิ่มจำนวนคลิกที่สร้างขึ้นโดยอัตโนมัติเป็น 2 เท่า
ตัวเลือกการซื้อแรกให้ประโยชน์โดยตรงแก่ผู้ใช้เป็น 2,000 ขีดกลาง ซึ่งจะพร้อมใช้งานสำหรับผู้ใช้โดยตรงและสามารถซื้อได้หลายครั้ง วิธีนี้เรียกว่าสินค้าอุปโภคบริโภค เพราะเป็นการบริโภคโดยตรงและบริโภคได้หลายครั้ง
ตัวเลือกที่ 2 จะอัปเกรดแดชบอร์ดให้สวยงามยิ่งขึ้น ต้องซื้อเพียงครั้งเดียวและใช้ได้ตลอดไป การซื้อดังกล่าวเรียกว่า "สินค้าอุปโภคบริโภค" เนื่องจากแอปใช้การซื้อไม่ได้แต่จะใช้ได้ตลอดไป
ตัวเลือกการซื้อครั้งที่ 3 ซึ่งเป็นครั้งสุดท้ายคือการสมัครใช้บริการ ขณะที่การสมัครรับข้อมูลใช้งานอยู่ ผู้ใช้จะได้รับขีดกลางเร็วขึ้น แต่เมื่อหยุดชำระค่าสมัครใช้บริการ สิทธิประโยชน์ก็จะหายไปด้วย
บริการแบ็กเอนด์ (มีให้คุณด้วย) จะทำงานเป็นแอป 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 โค้ดสำหรับ Codelab นี้คือ flutter-codelabs/in_app_purchases
โครงสร้างไดเรกทอรีภายใต้ flutter-codelabs/in_app_purchases
ประกอบด้วยชุดสแนปชอตของตำแหน่งที่คุณควรอยู่ท้ายขั้นตอนที่ตั้งชื่อแต่ละขั้น โค้ดเริ่มต้นอยู่ในขั้นตอนที่ 0 การค้นหาไฟล์ที่ตรงกันจึงง่ายมากดังนี้
cd flutter-codelabs/in_app_purchases/step_00
ถ้าคุณต้องการข้ามไปข้างหน้าหรือดูว่าควรมีลักษณะอย่างไรหลังจากขั้นตอนหนึ่ง ให้ดูในไดเรกทอรีที่ตั้งชื่อตามขั้นตอนที่คุณสนใจ โค้ดของขั้นตอนสุดท้ายอยู่ในโฟลเดอร์ complete
ตั้งค่าโปรเจ็กต์เริ่มต้น
เปิดโปรเจ็กต์เริ่มต้นจาก step_00
ใน IDE ที่คุณชื่นชอบ เราใช้ Android Studio เป็นภาพหน้าจอ แต่โค้ดของ Visual Studio ก็เป็นตัวเลือกที่ยอดเยี่ยมเช่นกัน เมื่อใช้โปรแกรมแก้ไขแบบใดแบบหนึ่ง ให้ตรวจสอบว่าได้ติดตั้งปลั๊กอิน Dart และ Flutter ล่าสุดแล้ว
แอปที่คุณจะต้องใช้ต้องสื่อสารกับ App Store และ Play Store เพื่อให้ทราบว่ามีผลิตภัณฑ์ใดบ้างที่ใช้ได้และมีราคาเท่าใด ทุกแอปจะได้รับการระบุด้วยรหัสที่ไม่ซ้ำกัน สำหรับ App Store ของ iOS รหัสนี้เรียกว่าตัวระบุชุด ส่วนสำหรับ Play Store บน Android รหัสนี้คือรหัสแอปพลิเคชัน ตัวระบุเหล่านี้มักจะสร้างโดยใช้รูปแบบชื่อโดเมนแบบย้อนกลับ ตัวอย่างเช่น เมื่อสร้างแอปการซื้อในแอปสำหรับ flutter.dev เราจะใช้ dev.flutter.inapppurchase
ลองนึกถึงตัวระบุสำหรับแอปของคุณ ตอนนี้คุณจะตั้งค่าดังกล่าวในการตั้งค่าโปรเจ็กต์
ขั้นแรก ให้ตั้งค่าตัวระบุกลุ่มสำหรับ iOS
เมื่อเปิดโปรเจ็กต์ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ iOS แล้วคลิก Flutter และเปิดโมดูลในแอป Xcode
ในโครงสร้างโฟลเดอร์ของ Xcode มีโปรเจ็กต์ Runner อยู่ที่ด้านบน และเป้าหมาย Flutter, Runner และผลิตภัณฑ์จะอยู่ใต้โปรเจ็กต์ Runner ดับเบิลคลิก Runner เพื่อแก้ไขการตั้งค่าโปรเจ็กต์ แล้วคลิก Signing & ความสามารถ ป้อนตัวระบุชุดซอฟต์แวร์ที่คุณเพิ่งเลือกในช่องทีมเพื่อตั้งค่าทีม
คุณสามารถปิด Xcode และกลับไปที่ Android Studio เพื่อกำหนดค่า Android ให้เสร็จได้แล้ว โดยเปิดไฟล์ build.gradle
ใน android/app,
และเปลี่ยน applicationId
(ในบรรทัดที่ 37 ในภาพหน้าจอด้านล่าง) เป็นรหัสแอปพลิเคชัน เช่นเดียวกับตัวระบุ Bundle ของ iOS โปรดทราบว่ารหัสสำหรับร้านค้าบน iOS และ Android ไม่จำเป็นต้องเหมือนกัน แต่รหัสที่เหมือนกันทุกประการจะมีโอกาสเกิดข้อผิดพลาดน้อยกว่า ดังนั้นใน Codelab นี้เราจะใช้ตัวระบุที่เหมือนกันด้วย
3. ติดตั้งปลั๊กอิน
คุณจะต้องติดตั้งปลั๊กอิน in_app_purchase ในส่วนนี้ของ Codelab
เพิ่มทรัพยากร Dependency ใน pubspec
เพิ่ม in_app_purchase
ลงใน pubspec โดยการเพิ่ม in_app_purchase
ไปยังทรัพยากร Dependency ใน pubspec ของคุณ:
$ cd app $ flutter pub add in_app_purchase
pubspec.yaml
dependencies:
..
cloud_firestore: ^4.0.3
firebase_auth: ^4.2.2
firebase_core: ^2.5.0
google_sign_in: ^6.0.1
http: ^0.13.4
in_app_purchase: ^3.0.1
intl: ^0.18.0
provider: ^6.0.2
..
คลิก pub get เพื่อดาวน์โหลดแพ็กเกจหรือเรียกใช้ flutter pub get
ในบรรทัดคำสั่ง
4. ตั้งค่า App Store
หากต้องการตั้งค่าการซื้อในแอปและทดสอบบน iOS คุณต้องสร้างแอปใหม่ใน App Store และสร้างผลิตภัณฑ์ที่ซื้อได้ในนั้น คุณไม่จำเป็นต้องเผยแพร่อะไรหรือส่งแอปให้ Apple ตรวจสอบเลย คุณต้องมีบัญชีนักพัฒนาซอฟต์แวร์จึงจะดำเนินการนี้ได้ หากยังไม่มี ให้ลงทะเบียนในโปรแกรมนักพัฒนาแอปของ Apple
ข้อตกลงสำหรับแอปแบบชำระเงิน
หากต้องการใช้การซื้อในแอป คุณยังต้องมีข้อตกลงที่มีผลอยู่สำหรับแอปที่ต้องซื้อใน App Store Connect ไปที่ https://appstoreconnect.apple.com/ แล้วคลิกข้อตกลง ภาษี และการธนาคาร
คุณจะเห็นข้อตกลงที่นี่สำหรับแอปฟรีและแอปที่ต้องซื้อ สถานะของแอปฟรีควรมีสถานะเป็นใช้งานอยู่ ส่วนแอปที่ต้องซื้อจะเป็นสถานะใหม่ โปรดตรวจสอบว่าคุณดู ยอมรับข้อกำหนด และป้อนข้อมูลที่จำเป็นทั้งหมดแล้ว
เมื่อตั้งค่าทุกอย่างอย่างถูกต้องแล้ว สถานะของแอปที่ต้องซื้อจะทำงาน ข้อนี้สำคัญมากเพราะคุณจะไม่สามารถลองใช้การซื้อในแอปได้หากไม่มีข้อตกลงที่มีผลอยู่
ลงทะเบียนรหัสแอป
สร้างตัวระบุใหม่ในพอร์ทัลนักพัฒนาซอฟต์แวร์ของ Apple
เลือกรหัสแอป
เลือกแอป
ระบุคําอธิบายและตั้งค่ารหัสชุดให้ตรงกับรหัสชุดให้ตรงกับค่าที่ตั้งไว้ก่อนหน้านี้ใน XCode
ดูคำแนะนำเพิ่มเติมเกี่ยวกับวิธีสร้างรหัสแอปใหม่ได้ที่ความช่วยเหลือเกี่ยวกับบัญชีนักพัฒนาแอป
การสร้างแอปใหม่
สร้างแอปใหม่ใน App Store Connect โดยใช้รหัสชุดที่ไม่ซ้ำกัน
ดูคำแนะนำเพิ่มเติมเกี่ยวกับวิธีสร้างแอปใหม่และจัดการข้อตกลงได้ที่ความช่วยเหลือเกี่ยวกับ App Store Connect
หากต้องการทดสอบการซื้อในแอป คุณต้องมีผู้ใช้ทดสอบแซนด์บ็อกซ์ ผู้ใช้ทดสอบรายนี้ไม่ควรเชื่อมต่อกับ iTunes เนื่องจากใช้สำหรับการทดสอบการซื้อในแอปเท่านั้น คุณไม่สามารถใช้อีเมลที่ใช้กับบัญชี Apple อยู่แล้ว ในส่วนผู้ใช้และการเข้าถึง ให้ไปที่ผู้ทดสอบในส่วนแซนด์บ็อกซ์เพื่อสร้างบัญชีแซนด์บ็อกซ์ใหม่หรือจัดการ Apple ID แซนด์บ็อกซ์ที่มีอยู่
ขณะนี้คุณสามารถตั้งค่าผู้ใช้แซนด์บ็อกซ์ของคุณบน iPhone ได้โดยไปที่การตั้งค่า > App Store > บัญชีแซนด์บ็อกซ์
การกำหนดค่าการซื้อในแอป
ต่อไป คุณจะต้องกำหนดค่ารายการที่ซื้อได้ 3 รายการ ดังนี้
dash_consumable_2k
: การซื้อสิ้นเปลืองที่ซื้อได้หลายครั้ง ซึ่งให้ขีดกลางยาว 2, 000 ขีด (สกุลเงินในแอป) แก่ผู้ใช้ต่อการซื้อ 1 ครั้งdash_upgrade_3d
: "การอัปเกรด" ที่ใช้ไม่ได้ ที่สามารถซื้อได้เพียงครั้งเดียว และทำให้ผู้ใช้สามารถคลิกดูหน้าแดชบอร์ดที่แตกต่างกันอย่างสวยงามdash_subscription_doubler
: การสมัครใช้บริการที่มอบขีดกลางต่อคลิกให้แก่ผู้ใช้เป็น 2 เท่าตลอดระยะเวลาการสมัครใช้บริการ
ไปที่การซื้อในแอป > จัดการ
สร้างการซื้อในแอปด้วยรหัสที่ระบุ:
- ตั้งค่า
dash_consumable_2k
เป็นอุปกรณ์สิ้นเปลือง
ใช้ dash_consumable_2k
เป็นรหัสผลิตภัณฑ์ ชื่อข้อมูลอ้างอิงจะใช้ในการเชื่อมต่อ App Store เท่านั้น เพียงตั้งค่าเป็น 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
คลิก Save
หลังจากคลิกปุ่มบันทึกแล้ว ให้เพิ่มราคาการสมัครใช้บริการ เลือกราคาที่คุณต้องการ
ตอนนี้คุณจะเห็นการซื้อ 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
รักษา
keystore
ไฟล์ส่วนตัว อย่าตรวจสอบเรื่องนี้ในการควบคุมแหล่งที่มาสาธารณะ
อ้างอิงคีย์สโตร์จากแอป
สร้างไฟล์ชื่อ <your app dir>/android/key.properties
ที่มีการอ้างอิงไปยังคีย์สโตร์ของคุณ:
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
กำหนดค่าการลงชื่อเข้าใช้ Gradle
กำหนดค่าการลงนามแอปโดยการแก้ไขไฟล์ <your app dir>/android/app/build.gradle
เพิ่มข้อมูลคีย์สโตร์จากไฟล์พร็อพเพอร์ตี้ก่อนบล็อก android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
โหลดไฟล์ key.properties
ลงในออบเจ็กต์ keystoreProperties
เพิ่มโค้ดต่อไปนี้ก่อนบล็อก buildTypes
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
กำหนดค่าบล็อก signingConfigs
ในไฟล์ build.gradle
ของโมดูลด้วยข้อมูลการกำหนดค่าการรับรองดังนี้
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
ตอนนี้ระบบจะรับรองบิลด์รุ่นของแอปโดยอัตโนมัติ
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการรับรองแอป โปรดดูลงนามแอปใน developer.android.com
อัปโหลดบิลด์แรก
หลังจากกำหนดค่าแอปสำหรับการรับรองแล้ว คุณควรสร้างแอปพลิเคชันได้โดยเรียกใช้
flutter build appbundle
คำสั่งนี้จะสร้างบิลด์รุ่นโดยค่าเริ่มต้นและดูเอาต์พุตได้ที่ <your app dir>/build/app/outputs/bundle/release/
จากหน้าแดชบอร์ดใน Google Play Console ให้ไปที่รุ่น > ทดสอบ > การทดสอบแบบปิด และสร้างรุ่นการทดสอบแบบปิดใหม่
สำหรับ Codelab นี้ คุณจะใช้ Google Signing ในแอปต่อไป ดังนั้นให้ดำเนินการต่อและกดดำเนินการต่อใต้ Play App Signing เพื่อเลือกใช้
จากนั้นอัปโหลด App Bundle app-release.aab
ที่สร้างขึ้นโดยคำสั่งบิลด์
คลิกบันทึก แล้วคลิกตรวจสอบรุ่น
สุดท้าย ให้คลิกเริ่มเปิดตัวในการทดสอบภายในเพื่อเปิดใช้งานรุ่นการทดสอบภายใน
ตั้งค่าผู้ใช้ทดสอบ
หากต้องการทดสอบการซื้อในแอป จะต้องเพิ่มบัญชี Google ของผู้ทดสอบใน Google Play Console จาก 2 ที่ต่อไปนี้
- ไปยังแทร็กทดสอบที่เฉพาะเจาะจง (การทดสอบภายใน)
- ในฐานะผู้ทดสอบใบอนุญาต
ก่อนอื่น ให้เริ่มจากการเพิ่มผู้ทดสอบลงในแทร็กทดสอบภายใน กลับไปที่รุ่น > ทดสอบ > การทดสอบภายใน แล้วคลิกแท็บผู้ทดสอบ
สร้างรายชื่ออีเมลใหม่โดยคลิกสร้างรายชื่ออีเมล ตั้งชื่อรายการ และเพิ่มอีเมลของบัญชี Google ที่ต้องเข้าถึงการทดสอบการซื้อในแอป
จากนั้นเลือกช่องทำเครื่องหมายของรายการ และคลิกบันทึกการเปลี่ยนแปลง
จากนั้นเพิ่มผู้ทดสอบใบอนุญาตโดยทำดังนี้
- กลับไปที่มุมมองแอปทั้งหมดของ Google Play Console
- ไปที่การตั้งค่า > การทดสอบใบอนุญาต
- เพิ่มอีเมลเดียวกันของผู้ทดสอบที่ต้องทดสอบการซื้อในแอปได้
- ตั้งค่าการตอบกลับเกี่ยวกับใบอนุญาตเป็น
RESPOND_NORMALLY
- คลิกบันทึกการเปลี่ยนแปลง
การกำหนดค่าการซื้อในแอป
ต่อไปนี้ คุณจะต้องกำหนดค่ารายการที่สามารถซื้อภายในแอปได้
เช่นเดียวกับใน App Store คุณจะต้องกำหนดการซื้อที่แตกต่างกัน 3 รายการดังนี้
dash_consumable_2k
: การซื้อสิ้นเปลืองที่ซื้อได้หลายครั้ง ซึ่งให้ขีดกลางยาว 2, 000 ขีด (สกุลเงินในแอป) แก่ผู้ใช้ต่อการซื้อ 1 ครั้งdash_upgrade_3d
: "การอัปเกรด" ที่ใช้ไม่ได้ ที่สามารถซื้อได้เพียงครั้งเดียว ซึ่งทำให้ผู้ใช้คลิกได้บน Dash ที่แตกต่างกันอย่างสวยงามdash_subscription_doubler
: การสมัครใช้บริการที่มอบขีดกลางต่อคลิกให้แก่ผู้ใช้เป็น 2 เท่าตลอดระยะเวลาการสมัครใช้บริการ
ขั้นแรก ให้เพิ่มรายการโฆษณาที่ใช้แล้วหมดไปแล้วและไม่ใช่ประเภทที่ใช้แล้วหมดไป
- ไปที่ Google Play Console แล้วเลือกแอปพลิเคชันของคุณ
- ไปที่สร้างรายได้ > ผลิตภัณฑ์ > ไอเทมที่ซื้อในแอป
- คลิกสร้างผลิตภัณฑ์
- ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับผลิตภัณฑ์ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณต้องการใช้ทั้งหมด
- คลิกบันทึก
- คลิกเปิดใช้งาน
- ทำขั้นตอนเดียวกันนี้อีกครั้งสำหรับ "การอัปเกรด" เวอร์ชันที่ใช้งานจริง การซื้อ
ถัดไป ให้เพิ่มการสมัครรับข้อมูลดังนี้
- ไปที่ Google Play Console แล้วเลือกแอปพลิเคชันของคุณ
- ไปที่สร้างรายได้ > ผลิตภัณฑ์ > การสมัครใช้บริการ
- คลิกสร้างการสมัครใช้บริการ
- ป้อนข้อมูลที่จำเป็นทั้งหมดสำหรับการสมัครใช้บริการ ตรวจสอบว่ารหัสผลิตภัณฑ์ตรงกับรหัสที่คุณต้องการใช้ทั้งหมด
- คลิกบันทึก
ตอนนี้คุณควรตั้งค่าการซื้อใน Play Console แล้ว
6. ตั้งค่า Firebase
ใน Codelab นี้ คุณจะใช้บริการแบ็กเอนด์เพื่อยืนยันและติดตามผู้ใช้ การซื้อ
การใช้บริการแบ็กเอนด์มีประโยชน์หลายประการดังนี้
- คุณสามารถยืนยันธุรกรรมได้อย่างปลอดภัย
- คุณตอบสนองต่อเหตุการณ์การเรียกเก็บเงินจาก App Store ได้
- คุณสามารถติดตามการซื้อได้ในฐานข้อมูล
- ผู้ใช้จะไม่สามารถหลอกให้แอปของคุณให้บริการฟีเจอร์พรีเมียมด้วยการกรอกลับนาฬิกาของระบบ
การตั้งค่าบริการแบ็กเอนด์มีหลายวิธี แต่คุณจะทำได้โดยใช้ฟังก์ชันระบบคลาวด์และ Firestore โดยใช้ Firebase ของ Google เอง
การเขียนแบ็กเอนด์ถือว่าอยู่นอกขอบเขตของ Codelab นี้ โค้ดเริ่มต้นจึงมีโปรเจ็กต์ Firebase ที่จัดการการซื้อพื้นฐานอยู่แล้วเพื่อให้คุณเริ่มต้นใช้งาน
ปลั๊กอิน Firebase รวมอยู่ในแอปเริ่มต้นด้วย
สิ่งสุดท้ายที่คุณต้องดำเนินการก็คือการสร้างโปรเจ็กต์ Firebase ของคุณเอง กำหนดค่าทั้งแอปและแบ็กเอนด์สำหรับ Firebase จากนั้นจึงทำให้แบ็กเอนด์ใช้งานได้
สร้างโปรเจ็กต์ Firebase
ไปที่คอนโซล Firebase แล้วสร้างโปรเจ็กต์ Firebase ใหม่ สำหรับตัวอย่างนี้ ให้เรียกโปรเจ็กต์ Dash Clicker
ในแอปแบ็กเอนด์ คุณจะต้องผูกการซื้อเข้ากับผู้ใช้ที่เจาะจง คุณจึงต้องมีการตรวจสอบสิทธิ์ ในกรณีนี้ ให้ใช้ประโยชน์จากโมดูลการตรวจสอบสิทธิ์ของ Firebase ด้วย Google Sign-In
- จากหน้าแดชบอร์ด 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 ให้เลือกโปรเจ็กต์ที่คุณเพิ่งสร้างในขั้นตอนก่อนหน้า
$ 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 Sign-In ในโหมดแก้ไขข้อบกพร่อง คุณต้องระบุแฮชลายนิ้วมือ SHA-1 ของใบรับรองการแก้ไขข้อบกพร่อง
รับแฮชใบรับรองที่มีการรับรองเพื่อแก้ไขข้อบกพร่อง
ในรูทของโปรเจ็กต์แอป Flutter ให้เปลี่ยนไดเรกทอรีเป็นโฟลเดอร์ android/
แล้วสร้างรายงานการรับรอง
cd android ./gradlew :app:signingReport
คุณจะเห็นรายการคีย์ Signing จำนวนมาก เนื่องจากคุณกำลังมองหาแฮชของใบรับรองการแก้ไขข้อบกพร่อง ให้มองหาใบรับรองที่มีการตั้งค่าพร็อพเพอร์ตี้ Variant
และ Config
เป็น debug
คีย์สโตร์อาจอยู่ในโฟลเดอร์หน้าแรกภายใต้ .android/debug.keystore
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
คัดลอกแฮช SHA-1 แล้วกรอกข้อมูลในช่องสุดท้ายในกล่องโต้ตอบโมดัลการส่งแอป
ตั้งค่า Firebase สำหรับ iOS: ขั้นตอนถัดไป
เปิด ios/Runnder.xcworkspace
ด้วย Xcode
หรือ IDE ที่คุณเลือกก็ได้
ใน VSCode ให้คลิกขวาที่โฟลเดอร์ ios/
แล้วคลิก open in xcode
ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ ios/
จากนั้นคลิก flutter
ตามด้วยตัวเลือก open iOS module in Xcode
หากต้องการให้ Google Sign-In ใน iOS ได้ ให้เพิ่มตัวเลือกการกำหนดค่า CFBundleURLTypes
ลงในไฟล์บิลด์ plist
(โปรดดูข้อมูลเพิ่มเติมในเอกสารแพ็กเกจ google_sign_in
) ในกรณีนี้ ไฟล์จะเป็น ios/Runner/Info-Debug.plist
และ ios/Runner/Info-Release.plist
เพิ่มคู่คีย์-ค่าแล้ว แต่ต้องแทนที่ค่าของคู่คีย์-ค่า
- รับค่าของ
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. ฟังข้อมูลอัปเดตเกี่ยวกับการซื้อ
ในส่วนนี้ คุณจะได้เตรียมแอปสำหรับการซื้อผลิตภัณฑ์ ขั้นตอนนี้จะรวมถึงการฟังการอัปเดตการซื้อและข้อผิดพลาดหลังจากที่แอปเริ่มทำงาน
ฟังข้อมูลอัปเดตเกี่ยวกับการซื้อ
ใน main.dart,
ให้ค้นหาวิดเจ็ต MyHomePage
ที่มี Scaffold
ที่มี BottomNavigationBar
ที่มี 2 หน้า หน้านี้ยังสร้าง Provider
3 รายการสำหรับ DashCounter
, DashUpgrades,
และ DashPurchases
DashCounter
จะติดตามจํานวนขีดกลางในปัจจุบันและจะเพิ่มขีดกลางโดยอัตโนมัติ DashUpgrades
จัดการการอัปเกรดที่คุณซื้อได้ด้วยขีดกลาง Codelab นี้มุ่งเน้นที่ DashPurchases
โดยค่าเริ่มต้น ระบบจะกำหนดออบเจ็กต์ของผู้ให้บริการเมื่อมีการขอออบเจ็กต์ดังกล่าวเป็นครั้งแรก ออบเจ็กต์นี้จะรอฟังการอัปเดตการซื้อโดยตรงเมื่อแอปเริ่มทำงาน ดังนั้นให้ปิดใช้การโหลดแบบ Lazy Loading ในออบเจ็กต์นี้ด้วย lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false,
),
คุณต้องมีอินสแตนซ์ของ InAppPurchaseConnection
ด้วย อย่างไรก็ตาม หากต้องการทดสอบแอปอยู่เสมอ คุณจะต้องมีวิธีการจำลองการเชื่อมต่อ ซึ่งทำได้โดยสร้างเมธอดของอินสแตนซ์ที่ลบล้างได้ในการทดสอบ แล้วเพิ่มไปยัง main.dart
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
คุณต้องอัปเดตการทดสอบเล็กน้อยหากต้องการให้การทดสอบทำงานต่อไป ดู widget_test.dart บน GitHub สำหรับโค้ดแบบเต็มสำหรับ TestIAPConnection
test/widget_test.dart
void main() {
testWidgets('App starts', (WidgetTester tester) async {
IAPConnection.instance = TestIAPConnection();
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
ใน lib/logic/dash_purchases.dart
ให้ไปที่รหัสของ DashPurchases ChangeNotifier
ปัจจุบันมีเพียง DashCounter
ที่คุณสามารถเพิ่มลงในขีดกลางที่ซื้อได้
เพิ่มพร็อพเพอร์ตี้การสมัครใช้บริการสตรีม _subscription
(ประเภท StreamSubscription<List<PurchaseDetails>> _subscription;
) IAPConnection.instance,
และการนําเข้า โค้ดที่ได้ควรมีลักษณะดังนี้
lib/logic/dash_purchases.dart
import 'package:in_app_purchase/in_app_purchase.dart';
class DashPurchases extends ChangeNotifier {
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter);
}
ระบบเพิ่มคีย์เวิร์ด late
ลงใน _subscription
เนื่องจาก _subscription
ได้รับการเริ่มต้นในตัวสร้าง โปรเจ็กต์นี้มีการตั้งค่าให้ไม่สามารถเว้นว่างได้โดยค่าเริ่มต้น (NNBD) ซึ่งหมายความว่าพร็อพเพอร์ตี้ที่ไม่ได้ประกาศเป็น Null ต้องมีค่าที่ไม่เป็น Null ตัวระบุ late
ช่วยให้คุณกำหนดค่านี้ล่าช้าได้
ในเครื่องมือสร้าง ให้ติดตั้ง purchaseUpdatedStream
และเริ่มฟังสตรีม ในเมธอด dispose()
ให้ยกเลิกการสมัครใช้บริการสตรีม
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
Future<void> buy(PurchasableProduct product) async {
// omitted
}
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
}
}
ตอนนี้แอปได้รับการอัปเดตการซื้อแล้ว ดังนั้นในส่วนถัดไป คุณจะได้ทำการซื้อ
ก่อนดำเนินการต่อ ให้ทดสอบกับ "flutter test"
" เพื่อยืนยันว่าตั้งค่าทุกอย่างถูกต้อง
$ flutter test
00:01 +1: All tests passed!
8. ซื้อสินค้าหรือบริการ
ในส่วนนี้ คุณจะแทนที่ผลิตภัณฑ์จำลองที่มีอยู่ด้วยผลิตภัณฑ์ที่ซื้อได้จริง ผลิตภัณฑ์เหล่านี้โหลดจากร้านค้าซึ่งแสดงในรายการและจะซื้อเมื่อแตะผลิตภัณฑ์
ปรับผลิตภัณฑ์ที่ซื้อได้
PurchasableProduct
แสดงผลิตภัณฑ์จำลอง โปรดอัปเดตโค้ดเพื่อแสดงเนื้อหาจริงโดยแทนที่คลาส PurchasableProduct
ใน purchasable_product.dart
ด้วยโค้ดต่อไปนี้
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
ใน dash_purchases.dart,
ให้นำการซื้อจำลองออกและแทนที่ด้วยรายการที่ว่างเปล่า List<PurchasableProduct> products = [];
โหลดรายการที่ซื้อได้
โหลดการซื้อจากร้านค้าเพื่อให้ผู้ใช้ทำการซื้อได้ ก่อนอื่น ให้ตรวจสอบว่าร้านค้าดังกล่าวพร้อมให้บริการหรือไม่ เมื่อร้านค้าไม่พร้อมให้บริการ การตั้งค่า storeState
เป็น notAvailable
จะแสดงข้อความแสดงข้อผิดพลาดให้แก่ผู้ใช้
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
เมื่อร้านค้าพร้อมให้บริการ ให้โหลดรายการที่ซื้อที่ใช้ได้ จากการตั้งค่า Firebase ก่อนหน้านี้ คุณจะเห็น storeKeyConsumable
, storeKeySubscription,
และ storeKeyUpgrade
เมื่อไม่มีการซื้อที่คาดไว้ ให้พิมพ์ข้อมูลนี้ไปยังคอนโซล เราขอแนะนำให้คุณส่งข้อมูลนี้ไปยังบริการแบ็กเอนด์ด้วย
เมธอด await iapConnection.queryProductDetails(ids)
จะแสดงทั้งรหัสที่ไม่พบและผลิตภัณฑ์ที่ซื้อได้ที่พบ ใช้ productDetails
จากคำตอบเพื่ออัปเดต UI และตั้งค่า StoreState
เป็น available
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
for (var element in response.notFoundIDs) {
debugPrint('Purchase $element not found');
}
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
เรียกฟังก์ชัน loadPurchases()
ในเครื่องมือสร้างดังนี้
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
สุดท้าย เปลี่ยนค่าของช่อง storeState
จาก StoreState.available
เป็น StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
แสดงผลิตภัณฑ์ที่ซื้อได้
และลองใช้ไฟล์ purchase_page.dart
วิดเจ็ต PurchasePage
จะแสดง _PurchasesLoading
, _PurchaseList,
หรือ _PurchasesNotAvailable,
ขึ้นอยู่กับ StoreState
วิดเจ็ตยังแสดงการซื้อที่ผ่านมาของผู้ใช้ซึ่งจะใช้ในขั้นตอนถัดไปด้วย
วิดเจ็ต _PurchaseList
จะแสดงรายการผลิตภัณฑ์ที่ซื้อได้และส่งคำขอซื้อไปยังออบเจ็กต์ DashPurchases
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
คุณควรเห็นผลิตภัณฑ์ที่พร้อมจำหน่ายในร้านค้า Android และ iOS หากกำหนดค่าอย่างถูกต้อง โปรดทราบว่าอาจใช้เวลาสักครู่ก่อนที่รายการที่ซื้อจะพร้อมใช้งานเมื่อป้อนลงในคอนโซลที่เกี่ยวข้อง
กลับไปที่ dash_purchases.dart
แล้วใช้ฟังก์ชันเพื่อซื้อผลิตภัณฑ์ คุณแค่ต้องแยกสินค้าอุปโภคบริโภคออกจากสินค้าบริโภคเท่านั้น การอัปเกรดและผลิตภัณฑ์ที่ต้องสมัครใช้บริการนั้นใช้งานไม่ได้
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
break;
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
ก่อนดำเนินการต่อ ให้สร้างตัวแปร _beautifiedDashUpgrade
และอัปเดต Getter ของ beautifiedDash
เพื่ออ้างอิง
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
เมธอด _onPurchaseUpdate
จะได้รับการอัปเดตการซื้อ อัปเดตสถานะของผลิตภัณฑ์ที่แสดงในหน้าการซื้อ และนำการซื้อไปใช้กับตรรกะตัวนับ คุณจะต้องโทรติดต่อ completePurchase
หลังจากจัดการการซื้อเพื่อให้ร้านค้าทราบว่าการซื้อได้รับการจัดการอย่างถูกต้อง
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(2000);
break;
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
break;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. ตั้งค่าแบ็กเอนด์
ก่อนจะไปต่อที่การติดตามและยืนยันการซื้อ ให้ตั้งค่าแบ็กเอนด์ของ DART เพื่อรองรับการดำเนินการดังกล่าว
ในส่วนนี้ ให้ดำเนินการจากโฟลเดอร์ dart-backend/
ในฐานะรูท
ตรวจสอบว่าคุณได้ติดตั้งเครื่องมือต่อไปนี้แล้ว
- Dart
- Firebase CLI
ภาพรวมโปรเจ็กต์พื้นฐาน
เนื่องจากบางส่วนของโครงการนี้ถือว่าอยู่นอกขอบเขตของ Codelab นี้ จึงรวมอยู่ในโค้ดเริ่มต้น คุณควรทบทวนสิ่งที่อยู่ในโค้ดเริ่มต้นอยู่แล้วก่อนที่จะเริ่มต้น เพื่อให้ทราบว่าคุณจะวางโครงสร้างของสิ่งต่างๆ อย่างไร
โค้ดแบ็กเอนด์นี้สามารถเรียกใช้ภายในเครื่องของคุณได้ คุณไม่จำเป็นต้องติดตั้งใช้งานโค้ด อย่างไรก็ตาม คุณจำเป็นต้องสามารถเชื่อมต่อจากอุปกรณ์การพัฒนา (Android หรือ iPhone) ของคุณเข้ากับเครื่องที่เซิร์ฟเวอร์จะทำงาน โดยเครื่องจะต้องอยู่ในเครือข่ายเดียวกัน และคุณจำเป็นต้องทราบที่อยู่ IP ของเครื่อง
ลองเรียกใช้เซิร์ฟเวอร์ด้วยคำสั่งต่อไปนี้
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
แบ็กเอนด์ของ DART ใช้ shelf
และ shelf_router
เพื่อแสดงปลายทาง API โดยค่าเริ่มต้น เซิร์ฟเวอร์จะไม่ให้เส้นทางใดๆ หลังจากนั้น คุณจะสร้างเส้นทางเพื่อจัดการกระบวนการยืนยันการซื้อ
ส่วนหนึ่งที่รวมอยู่ในโค้ดเริ่มต้นอยู่แล้วคือ IapRepository
ใน lib/iap_repository.dart
เนื่องจากการเรียนรู้วิธีโต้ตอบกับ Firestore หรือฐานข้อมูลโดยทั่วไปไม่ถือว่าเกี่ยวข้องกับ Codelab นี้ โค้ดเริ่มต้นจึงมีฟังก์ชันให้คุณสร้างหรืออัปเดตการซื้อใน Firestore รวมถึงคลาสทั้งหมดสำหรับการซื้อเหล่านั้น
ตั้งค่าการเข้าถึง Firebase
หากต้องการเข้าถึง Firebase Firestore คุณต้องมีคีย์สิทธิ์เข้าถึงบัญชีบริการ สร้าง 1 รายการโดยเปิดการตั้งค่าโปรเจ็กต์ Firebase แล้วไปที่ส่วนบัญชีบริการ จากนั้นเลือกสร้างคีย์ส่วนตัวใหม่
คัดลอกไฟล์ JSON ที่ดาวน์โหลดไปยังโฟลเดอร์ assets/
และเปลี่ยนชื่อเป็น service-account-firebase.json
ตั้งค่าการเข้าถึง Google Play
หากต้องการเข้าถึง Play Store เพื่อยืนยันการซื้อ คุณต้องสร้างบัญชีบริการที่มีสิทธิ์เหล่านี้และดาวน์โหลดข้อมูลเข้าสู่ระบบ JSON สำหรับบัญชีดังกล่าว
- ไปที่ Google Play Console แล้วเริ่มจากหน้าแอปทั้งหมด
- ไปที่การตั้งค่า > การเข้าถึง API ในกรณีที่ Google Play Console ขอให้คุณสร้างหรือลิงก์กับโปรเจ็กต์ที่มีอยู่ ให้ทำก่อนแล้วจึงกลับมาที่หน้านี้
- ค้นหาส่วนที่คุณกำหนดบัญชีบริการ แล้วคลิกสร้างบัญชีบริการใหม่
- คลิกลิงก์ Google Cloud Platform ในกล่องโต้ตอบที่ปรากฏขึ้น
- เลือกโปรเจ็กต์ หากไม่เห็น ให้ตรวจสอบว่าคุณได้ลงชื่อเข้าใช้บัญชี Google ที่ถูกต้องในรายการแบบเลื่อนลงบัญชีที่ด้านบนขวา
- หลังจากเลือกโปรเจ็กต์แล้ว ให้คลิก + สร้างบัญชีบริการในแถบเมนูด้านบน
- ระบุชื่อบัญชีบริการ ใส่คำอธิบายหรือไม่ก็ได้ เพื่อให้คุณจดจำได้สำหรับบัญชีบริการ แล้วไปยังขั้นตอนถัดไป
- มอบหมายบทบาทผู้แก้ไขให้กับบัญชีบริการ
- ดำเนินการวิซาร์ดให้เสร็จ แล้วกลับไปที่หน้าการเข้าถึง API ใน Developer Console แล้วคลิกรีเฟรชบัญชีบริการ คุณจะเห็นบัญชีที่สร้างใหม่ในรายการ
- คลิกให้สิทธิ์เข้าถึงสำหรับบัญชีบริการใหม่
- เลื่อนลงในหน้าถัดไปที่บล็อกข้อมูลทางการเงิน เลือกทั้งดูข้อมูลทางการเงิน คำสั่งซื้อ และการตอบแบบสำรวจการยกเลิกและจัดการคำสั่งซื้อและการสมัครใช้บริการ
- คลิกเชิญผู้ใช้
- หลังจากที่ตั้งค่าบัญชีแล้ว คุณก็เพียงแค่สร้างข้อมูลเข้าสู่ระบบบางอย่าง กลับไปที่ Cloud Console ค้นหาบัญชีบริการในรายการบัญชีบริการ คลิกจุดแนวตั้ง 3 จุด แล้วเลือกจัดการคีย์
- สร้างคีย์ JSON ใหม่และดาวน์โหลด
- เปลี่ยนชื่อไฟล์ที่ดาวน์โหลดเป็น
service-account-google-play.json,
แล้วย้ายไปยังไดเรกทอรีassets/
อีกสิ่งหนึ่งที่เราต้องทำคือเปิด lib/constants.dart,
และแทนที่ค่าของ androidPackageId
ด้วยรหัสแพ็กเกจที่คุณเลือกสำหรับแอป Android
ตั้งค่าการเข้าถึง Apple App Store
หากต้องการเข้าถึง App Store เพื่อยืนยันการซื้อ คุณต้องตั้งค่าข้อมูลลับที่ใช้ร่วมกันดังนี้
- เปิด App Store Connect
- ไปที่แอปของฉัน แล้วเลือกแอป
- ในแถบนำทางด้านข้าง ให้ไปที่การซื้อในแอป > จัดการ
- คลิก App-Specific Shared Secret ที่ด้านขวาบนของรายการ
- สร้างข้อมูลลับใหม่แล้วคัดลอก
- เปิด
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 '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
ปลายทางการยืนยันการโทรจากแอป
ในแอป ให้สร้างฟังก์ชัน _verifyPurchase(PurchaseDetails purchaseDetails)
ที่เรียกใช้ปลายทาง /verifypurchase
บนแบ็กเอนด์ของ DART โดยใช้การเรียกหลัง HTTP
ส่ง Store ที่เลือก (google_play
สำหรับ Play Store หรือ app_store
สำหรับ App Store), serverVerificationData
และ productID
เซิร์ฟเวอร์จะแสดงรหัสสถานะที่ระบุว่าการซื้อได้รับการยืนยันแล้วหรือไม่
ในการกำหนดค่าคงที่ของแอป ให้กำหนดค่า IP ของเซิร์ฟเวอร์ไปยังที่อยู่ IP ของเครื่องภายใน
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
เพิ่ม firebaseNotifier
ที่มีการสร้าง DashPurchases
ใน main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
เพิ่ม Getter สำหรับผู้ใช้ใน FirebaseNotifier เพื่อให้คุณสามารถส่งต่อ User ID ไปยังฟังก์ชันยืนยันการซื้อ
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
เพิ่มฟังก์ชัน _verifyPurchase
ในคลาส DashPurchases
ฟังก์ชัน async
นี้จะแสดงบูลีนที่ระบุว่าการซื้อได้รับการตรวจสอบแล้วหรือไม่
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
print('Successfully verified purchase');
return true;
} else {
print('failed request: ${response.statusCode} - ${response.body}');
return false;
}
}
เรียกใช้ฟังก์ชัน _verifyPurchase
ใน _handlePurchase
ก่อนที่จะใช้การซื้อ คุณควรใช้การซื้อเมื่อการซื้อได้รับการยืนยันแล้วเท่านั้น ในแอปเวอร์ชันที่ใช้งานจริง คุณจะระบุเพิ่มเติมได้อีก เช่น ใช้การสมัครใช้บริการแบบทดลองใช้เมื่อ Store ไม่พร้อมให้บริการชั่วคราว อย่างไรก็ตาม สำหรับตัวอย่างนี้ เรียบง่ายและใช้การซื้อเฉพาะเมื่อการซื้อได้รับการยืนยันเรียบร้อยแล้วเท่านั้น
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(1000);
break;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
ทุกอย่างในแอปพร้อมตรวจสอบการซื้อแล้ว
ตั้งค่าบริการแบ็กเอนด์
ต่อไป ให้ตั้งค่าฟังก์ชันระบบคลาวด์เพื่อยืนยันการซื้อบนแบ็กเอนด์
สร้างเครื่องจัดการการซื้อ
เนื่องจากขั้นตอนการยืนยันสำหรับร้านค้าทั้ง 2 แห่งมีความคล้ายคลึงกัน โปรดตั้งค่าคลาส PurchaseHandler
แบบนามธรรมที่มีการติดตั้งใช้งานแยกกันสำหรับแต่ละร้านค้า
เริ่มต้นด้วยการเพิ่มไฟล์ purchase_handler.dart
ไปยังโฟลเดอร์ lib/
ซึ่งคุณจะกําหนดคลาส PurchaseHandler
แบบนามธรรมที่มีวิธีการแบบนามธรรม 2 แบบในการยืนยันการซื้อที่แตกต่างกัน 2 ประเภท ได้แก่ การสมัครใช้บริการและการไม่สมัครใช้บริการ
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
ดังที่คุณเห็น แต่ละเมธอดต้องมีพารามิเตอร์ 3 ตัว ดังนี้
userId:
รหัสของผู้ใช้ที่เข้าสู่ระบบเพื่อให้คุณผูกการซื้อกับผู้ใช้ได้productData:
ข้อมูลเกี่ยวกับผลิตภัณฑ์ คุณจะกำหนดสิ่งนี้ในอีก 1 นาทีtoken:
โทเค็นที่ร้านค้าให้ผู้ใช้
นอกจากนี้ เพื่อให้เครื่องจัดการการซื้อเหล่านี้ใช้งานง่ายยิ่งขึ้น ให้เพิ่มเมธอด verifyPurchase()
ที่ใช้ได้ทั้งสำหรับการสมัครใช้บริการและการที่ไม่ใช่การสมัครใช้บริการ ดังนี้
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
ตอนนี้คุณสามารถเรียกใช้ verifyPurchase
สำหรับทั้ง 2 กรณีได้เลย แต่ยังคงมีการติดตั้งใช้งานที่แยกกัน
คลาส ProductData
มีข้อมูลพื้นฐานเกี่ยวกับผลิตภัณฑ์ต่างๆ ที่ซื้อได้ ซึ่งรวมถึงรหัสผลิตภัณฑ์ (บางครั้งเรียกว่า SKU) และ ProductType
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ProductType
อาจเป็นการสมัครใช้บริการหรือไม่สมัครใช้บริการก็ได้
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
สุดท้าย รายการผลิตภัณฑ์จะได้รับการกำหนดเป็นแผนที่ในไฟล์เดียวกัน
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
ถัดไป ให้กำหนดการใช้งานตัวยึดตำแหน่งบางอย่างสำหรับ Google Play Store และ Apple App Store เริ่มต้นด้วย Google Play
สร้าง lib/google_play_purchase_handler.dart
และเพิ่มชั้นเรียนที่ขยาย PurchaseHandler
ที่คุณเพิ่งเขียน:
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
ในขณะนี้ จะแสดง true
สำหรับเมธอดของตัวแฮนเดิล แล้วระบบจะนำคุณไปที่ลิงก์นั้นในภายหลัง
คุณอาจสังเกตเห็นแล้วว่าตัวสร้างใช้อินสแตนซ์ของ IapRepository
เครื่องจัดการการซื้อจะใช้อินสแตนซ์นี้เพื่อเก็บข้อมูลเกี่ยวกับการซื้อใน Firestore ในภายหลัง หากต้องการสื่อสารกับ Google Play ให้ใช้AndroidPublisherApi
ที่ให้ไว้
ถัดไป ให้ทำแบบเดียวกันนี้กับเครื่องจัดการ App Store สร้าง lib/app_store_purchase_handler.dart
และเพิ่มชั้นเรียนที่ขยาย PurchaseHandler
อีกครั้ง:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
เยี่ยม! ตอนนี้คุณมีเครื่องจัดการการซื้อ 2 รายการแล้ว ถัดไป ให้สร้างปลายทาง API การยืนยันการซื้อ
ใช้เครื่องจัดการการซื้อ
เปิด bin/server.dart
และสร้างปลายทาง API โดยใช้ shelf_route
:
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
โค้ดด้านบนกำลังทำสิ่งต่อไปนี้
- กำหนดปลายทาง 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
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
ตอนนี้การซื้อใน Google Play ควรได้รับการยืนยันและจัดเก็บไว้ในฐานข้อมูลแล้ว
ต่อไปให้ไปที่การซื้อใน App Store สำหรับ iOS
ยืนยันการซื้อใน iOS: ใช้เครื่องจัดการการซื้อ
สำหรับการยืนยันการซื้อด้วย App Store จะมีแพ็กเกจ Dart ของบุคคลที่สามชื่อ app_store_server_sdk
อยู่ ซึ่งทำให้กระบวนการนี้ง่ายขึ้น
เริ่มต้นด้วยการสร้างอินสแตนซ์ ITunesApi
ใช้การกำหนดค่าแซนด์บ็อกซ์และเปิดใช้การบันทึกเพื่อช่วยให้แก้ไขข้อบกพร่องข้อผิดพลาดได้ง่ายขึ้น
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
ซึ่งตอนนี้ App Store ต่างจาก Google Play API ตรงที่ใช้ปลายทาง API เดียวกันสำหรับการสมัครใช้บริการและไม่ได้สมัครใช้บริการ ซึ่งหมายความว่าคุณสามารถใช้ตรรกะเดียวกันสำหรับตัวจัดการทั้ง 2 ตัวได้ ผสานรวมพร็อพเพอร์ตี้เหล่านั้นเข้าด้วยกันเพื่อให้เรียกการติดตั้งใช้งานเดียวกัน นั่นคือ
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
ตอนนี้ ให้ใช้ handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
print('Successfully verified purchase');
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
การซื้อใน App Store ควรได้รับการยืนยันและจัดเก็บไว้ในฐานข้อมูลแล้ว
เรียกใช้แบ็กเอนด์
ณ จุดนี้ คุณสามารถเรียกใช้ dart bin/server.dart
เพื่อให้บริการปลายทาง /verifypurchase
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. ติดตามการซื้อ
วิธีที่แนะนำในการติดตามผู้ใช้ของคุณ การซื้อจะอยู่ในบริการแบ็กเอนด์ ทั้งนี้เนื่องจากแบ็กเอนด์ของคุณสามารถตอบสนองต่อเหตุการณ์จาก Store ได้ จึงมีแนวโน้มที่จะรับข้อมูลที่ล้าสมัยน้อยลงเนื่องจากการแคช รวมถึงมีความเสี่ยงที่จะถูกดัดแปลงน้อยกว่าด้วย
ก่อนอื่นให้ตั้งค่าการประมวลผลเหตุการณ์ของร้านค้าบนแบ็กเอนด์ด้วยแบ็กเอนด์ของ DART ที่คุณกำลังสร้าง
ประมวลผลเหตุการณ์ของร้านค้าในแบ็กเอนด์
ร้านค้าสามารถแจ้งแบ็กเอนด์เกี่ยวกับเหตุการณ์การเรียกเก็บเงินที่เกิดขึ้นได้ เช่น เมื่อต่ออายุการสมัครใช้บริการ คุณสามารถประมวลผลเหตุการณ์เหล่านี้ในแบ็กเอนด์เพื่อให้การซื้อในฐานข้อมูลเป็นปัจจุบันอยู่เสมอ ในส่วนนี้ ให้ตั้งค่านี้สำหรับทั้ง Google Play Store และ Apple App Store
ประมวลผลกิจกรรมการเรียกเก็บเงินของ Google Play
Google Play ให้บริการกิจกรรมการเรียกเก็บเงินผ่านสิ่งที่เรียกว่าหัวข้อ Pub/Sub ของระบบคลาวด์ โดยพื้นฐานแล้วคือคิวข้อความที่สามารถใช้เผยแพร่และเรียกดูข้อความได้
เนื่องจากนี่เป็นฟังก์ชันการทำงานเฉพาะสำหรับ Google Play คุณจึงรวมฟังก์ชันนี้ไว้ในGooglePlayPurchaseHandler
เริ่มต้นด้วยการเปิด lib/google_play_purchase_handler.dart
และเพิ่มการนำเข้า PubsubApi ดังนี้
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
จากนั้นส่ง PubsubApi
ไปยัง GooglePlayPurchaseHandler
และแก้ไขตัวสร้างคลาสเพื่อสร้าง Timer
ดังนี้
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
มีการกำหนดค่า Timer
ให้เรียกใช้เมธอด _pullMessageFromSubSub
ทุก 10 วินาที คุณสามารถปรับระยะเวลาได้ตามต้องการ
จากนั้นสร้าง _pullMessageFromSubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
รหัสที่คุณเพิ่งเพิ่มจะสื่อสารกับหัวข้อ Pub/Sub จาก Google Cloud ทุกๆ 10 วินาทีและขอข้อความใหม่ จากนั้นจึงประมวลผลแต่ละข้อความในเมธอด _processMessage
วิธีการนี้จะถอดรหัสข้อความขาเข้าและรับข้อมูลที่อัปเดตเกี่ยวกับการซื้อแต่ละครั้ง ทั้งการสมัครรับข้อมูลและการที่ไม่ใช่การสมัครรับข้อมูล โดยจะเรียกใช้ handleSubscription
หรือ handleNonSubscription
ที่มีอยู่หากจำเป็น
คุณต้องรับทราบแต่ละข้อความโดยใช้เมธอด _askMessage
จากนั้นเพิ่มทรัพยากร Dependency ที่จำเป็นลงในไฟล์ server.dart
เพิ่ม PubsubApi.cloudPlatformScope ในการกำหนดค่าข้อมูลเข้าสู่ระบบ ดังนี้
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
จากนั้นสร้างอินสแตนซ์ PubsubApi ด้วยคำสั่งต่อไปนี้
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
และสุดท้ายให้ส่งต่อให้กับตัวสร้าง GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
การตั้งค่า Google Play
คุณได้เขียนโค้ดเพื่อใช้กิจกรรมการเรียกเก็บเงินจากหัวข้อ pub/sub แต่ยังไม่ได้สร้างหัวข้อ pub/sub และคุณไม่ได้เผยแพร่กิจกรรมการเรียกเก็บเงินใดๆ ได้เวลาตั้งค่านี้แล้ว
ขั้นแรก ให้สร้างหัวข้อ pub/sub โดยทำดังนี้
- ไปที่หน้า Cloud Pub/Sub บน Google Cloud Console
- ตรวจสอบว่าคุณอยู่ในโปรเจ็กต์ Firebase แล้วคลิก + สร้างหัวข้อ
- ตั้งชื่อหัวข้อใหม่ให้เหมือนกับค่าที่ตั้งไว้สำหรับ
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
ในconstants.ts
ในกรณีนี้ ให้ตั้งชื่อว่าplay_billing
หากเลือกรายการอื่น อย่าลืมอัปเดตconstants.ts
สร้างหัวข้อ - ในรายการหัวข้อ Pub/Sub ให้คลิกจุดแนวตั้ง 3 จุดสำหรับหัวข้อที่คุณเพิ่งสร้าง แล้วคลิกดูสิทธิ์
- ในแถบด้านข้างทางขวา ให้เลือกเพิ่มผู้ใช้หลัก
- ตรงนี้ ให้เพิ่ม
google-play-developer-notifications@system.gserviceaccount.com
และมอบบทบาทผู้เผยแพร่ Pub/Sub - บันทึกการเปลี่ยนแปลงสิทธิ์
- คัดลอกชื่อหัวข้อของหัวข้อที่คุณเพิ่งสร้าง
- เปิด Play Console อีกครั้ง แล้วเลือกแอปจากรายการแอปทั้งหมด
- เลื่อนลงและไปที่สร้างรายได้ > การตั้งค่าการสร้างรายได้
- กรอกหัวข้อทั้งหมดและบันทึกการเปลี่ยนแปลง
ตอนนี้เหตุการณ์ทั้งหมดใน Google Play Billing จะได้รับการเผยแพร่เกี่ยวกับหัวข้อนี้
ประมวลผลเหตุการณ์การเรียกเก็บเงินของ App Store
ให้ทำแบบเดียวกันนี้สำหรับกิจกรรมการเรียกเก็บเงินของ App Store วิธีที่มีประสิทธิภาพในการจัดการการอัปเดตสำหรับการซื้อ App Store มีอยู่ 2 วิธีด้วยกัน หนึ่งในนั้นคือการใช้เว็บฮุคที่คุณให้กับ Apple ซึ่งทำหน้าที่สื่อสารกับเซิร์ฟเวอร์ของคุณ วิธีที่ 2 ซึ่งคุณจะพบใน Codelab นี้ก็คือการเชื่อมต่อกับ App Store Server API และรับข้อมูลการสมัครใช้บริการด้วยตนเอง
เหตุผลที่ Codelab นี้มุ่งเน้นโซลูชันที่ 2 ก็เพราะว่าคุณจะต้องเปิดเผยเซิร์ฟเวอร์ต่ออินเทอร์เน็ตเพื่อใช้งานเว็บฮุค
ในสภาพแวดล้อมการใช้งานจริง คุณควรมีทั้ง 2 อย่าง เว็บฮุคสำหรับรับเหตุการณ์จาก App Store และ Server API ในกรณีที่คุณไม่มีกิจกรรมหรือต้องการตรวจสอบสถานะการสมัครใช้บริการอีกครั้ง
เริ่มต้นด้วยการเปิด lib/app_store_purchase_handler.dart
และเพิ่มทรัพยากร Dependency ของ AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
แก้ไขเครื่องมือสร้างเพื่อเพิ่มตัวจับเวลาที่จะเรียกเมธอด _pullStatus
ตัวจับเวลานี้จะเรียกใช้เมธอด _pullStatus
ทุก 10 วินาที คุณปรับระยะเวลาของตัวจับเวลานี้ได้ตามความต้องการ
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
จากนั้นสร้างเมธอด _pullStatus ดังนี้
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
วิธีการนี้มีการทำงานดังนี้
- รับรายการการสมัครใช้บริการที่ใช้งานอยู่จาก Firestore โดยใช้ IapRepository
- และจะขอสถานะการสมัครใช้บริการ App Store Server API สำหรับคำสั่งซื้อแต่ละรายการ
- รับธุรกรรมล่าสุดสำหรับการซื้อการสมัครใช้บริการนั้นๆ
- ตรวจสอบวันที่หมดอายุ
- อัปเดตสถานะการสมัครใช้บริการใน Firestore หากหมดอายุ ระบบจะทำเครื่องหมายให้ทราบ
สุดท้าย เพิ่มโค้ดที่จำเป็นทั้งหมดเพื่อกำหนดค่าการเข้าถึง API ของเซิร์ฟเวอร์ App Store ดังนี้
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
การตั้งค่า App Store
ต่อไป ให้ตั้งค่า App Store ดังนี้
- เข้าสู่ระบบ 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
ในตัวสร้าง ถัดไป ให้เพิ่ม Listener ในตัวสร้างโดยตรง และนำ Listener ออกในเมธอด dispose()
ในช่วงแรก Listener จะเป็นแค่ฟังก์ชันว่างได้ เนื่องจาก IAPRepo
เป็น ChangeNotifier
และคุณเรียกใช้ notifyListeners()
ทุกครั้งที่การซื้อใน Firestore มีการเปลี่ยนแปลง เมธอด purchasesUpdate()
จะถูกเรียกเสมอเมื่อผลิตภัณฑ์ที่ซื้อมีการเปลี่ยนแปลง
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
iapRepo.removeListener(purchasesUpdate);
_subscription.cancel();
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
ถัดไป ให้ระบุ IAPRepo
ให้กับเครื่องมือสร้างใน main.dart.
คุณจะรับที่เก็บได้โดยใช้ context.read
เนื่องจากมีการสร้างไว้ใน Provider
แล้ว
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
ถัดไป ให้เขียนโค้ดสำหรับฟังก์ชัน purchaseUpdate()
ใน dash_counter.dart,
เมธอด applyPaidMultiplier
และ removePaidMultiplier
จะตั้งค่าตัวคูณเป็น 10 หรือ 1 ตามลำดับ คุณจึงไม่จำเป็นต้องตรวจสอบว่าได้ใช้การสมัครใช้บริการแล้วหรือไม่ เมื่อสถานะการสมัครใช้บริการมีการเปลี่ยนแปลง คุณยังอัปเดตสถานะของผลิตภัณฑ์ที่ซื้อได้เพื่อแสดงในหน้าการซื้อว่าผลิตภัณฑ์ดังกล่าวมีการใช้งานอยู่ด้วย ตั้งค่าพร็อพเพอร์ตี้ _beautifiedDashUpgrade
ตามการซื้อการอัปเกรด
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
ตอนนี้คุณก็ทำให้สถานะการสมัครใช้บริการและการอัปเกรดเป็นข้อมูลล่าสุดเสมอในบริการแบ็กเอนด์และซิงค์กับแอปแล้ว แอปจะทำงานตามนั้นและใช้ฟีเจอร์การสมัครใช้บริการและการอัปเกรดกับเกม Dasher ของคุณ
12. เสร็จเรียบร้อย
ขอแสดงความยินดี! คุณทำ Codelab เสร็จสมบูรณ์แล้ว คุณดูโค้ดที่เสร็จสมบูรณ์ของ Codelab ได้ในโฟลเดอร์ที่เสร็จสมบูรณ์
ลองดูข้อมูลเพิ่มเติมที่ Flutter Codelab