1. Pengantar
Menambahkan pembelian dalam aplikasi ke aplikasi Flutter memerlukan penyiapan App Store dan Play Store dengan benar, memverifikasi pembelian, dan memberikan izin yang diperlukan, seperti keuntungan langganan.
Dalam codelab ini, Anda akan menambahkan tiga jenis pembelian dalam aplikasi ke aplikasi (disediakan untuk Anda), dan memverifikasi pembelian ini menggunakan backend Dart dengan Firebase. Aplikasi yang disediakan, Dash Clicker, berisi game yang menggunakan maskot Dash sebagai mata uang. Anda akan menambahkan opsi pembelian berikut:
- Opsi pembelian berulang untuk 2.000 Dash sekaligus.
- Pembelian upgrade satu kali untuk mengubah Dash bergaya lama menjadi Dash bergaya modern.
- Langganan yang menggandakan klik yang dibuat secara otomatis.
Opsi pembelian pertama memberi pengguna manfaat langsung sebesar 2.000 Dash. Item ini tersedia langsung untuk pengguna dan dapat dibeli berkali-kali. Ini disebut habis pakai karena langsung digunakan dan dapat digunakan beberapa kali.
Opsi kedua mengupgrade Dash menjadi Dash yang lebih menarik. Paket ini hanya perlu dibeli satu kali dan tersedia selamanya. Pembelian tersebut disebut non-consumable karena tidak dapat digunakan oleh aplikasi, tetapi valid selamanya.
Opsi pembelian ketiga dan terakhir adalah langganan. Saat langganan aktif, pengguna akan mendapatkan Dash lebih cepat, tetapi saat dia berhenti membayar langganan, manfaatnya juga akan hilang.
Layanan backend (juga disediakan untuk Anda) berjalan sebagai aplikasi Dart, memverifikasi bahwa pembelian dilakukan, dan menyimpannya menggunakan Firestore. Firestore digunakan untuk mempermudah prosesnya, tetapi di aplikasi produksi, Anda dapat menggunakan jenis layanan backend apa pun.
Yang akan Anda build
- Anda akan memperluas aplikasi untuk mendukung pembelian dan langganan yang dapat digunakan.
- Anda juga akan memperluas aplikasi backend Dart untuk memverifikasi dan menyimpan item yang dibeli.
Yang akan Anda pelajari
- Cara mengonfigurasi App Store dan Play Store dengan produk yang dapat dibeli.
- Cara berkomunikasi dengan toko untuk memverifikasi pembelian dan menyimpannya di Firestore.
- Cara mengelola pembelian di aplikasi Anda.
Yang Anda butuhkan
- Android Studio 4.1 atau yang lebih baru
- Xcode versi 12 atau yang lebih baru (untuk pengembangan iOS)
- Flutter SDK
2. Menyiapkan lingkungan pengembangan
Untuk memulai codelab ini, download kode dan ubah ID paket untuk iOS dan nama paket untuk Android.
Mendownload kode
Untuk meng-clone repositori GitHub dari command line, gunakan perintah berikut:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Atau, jika Anda telah menginstal alat cli GitHub, gunakan perintah berikut:
gh repo clone flutter/codelabs flutter-codelabs
Kode contoh di-clone ke direktori flutter-codelabs
yang berisi kode untuk kumpulan codelab. Kode untuk codelab ini ada di flutter-codelabs/in_app_purchases
.
Struktur direktori di bagian flutter-codelabs/in_app_purchases
berisi serangkaian snapshot tentang posisi Anda di akhir setiap langkah yang diberi nama. Kode awal berada di langkah 0, sehingga menemukan file yang cocok sangat mudah:
cd flutter-codelabs/in_app_purchases/step_00
Jika Anda ingin melompat ke bagian berikutnya atau melihat tampilan sesuatu setelah suatu langkah, lihat di direktori yang diberi nama sesuai langkah yang Anda minati. Kode langkah terakhir ada di folder complete
.
Menyiapkan project awal
Buka project awal dari step_00
di IDE favorit Anda. Kami menggunakan Android Studio untuk screenshot, tetapi Visual Studio Code juga merupakan opsi yang bagus. Dengan editor mana pun, pastikan plugin Dart dan Flutter terbaru telah diinstal.
Aplikasi yang akan Anda buat harus berkomunikasi dengan App Store dan Play Store untuk mengetahui produk mana yang tersedia dan harganya. Setiap aplikasi diidentifikasi dengan ID unik. Untuk App Store iOS, ID ini disebut ID paket, dan untuk Android Play Store, ID ini adalah ID aplikasi. ID ini biasanya dibuat menggunakan notasi nama domain terbalik. Misalnya, saat membuat aplikasi pembelian dalam aplikasi untuk flutter.dev, Anda akan menggunakan dev.flutter.inapppurchase
. Pikirkan ID untuk aplikasi Anda, sekarang Anda akan menetapkannya di setelan project.
Pertama, siapkan ID paket untuk iOS.
Dengan project yang terbuka di Android Studio, klik kanan folder iOS, klik Flutter, lalu buka modul di aplikasi Xcode.
Dalam struktur folder Xcode, project Runner berada di bagian atas, dan target Flutter, Runner, dan Products berada di bawah project Runner. Klik dua kali Runner untuk mengedit setelan project, lalu klik Signing & Capabilities. Masukkan ID paket yang baru saja Anda pilih di kolom Tim untuk menetapkan tim.
Sekarang Anda dapat menutup Xcode dan kembali ke Android Studio untuk menyelesaikan konfigurasi untuk Android. Untuk melakukannya, buka file build.gradle
di bagian android/app,
dan ubah applicationId
(di baris 37 pada screenshot di bawah) menjadi ID aplikasi, sama seperti ID paket iOS. Perhatikan bahwa ID untuk iOS dan Android Store tidak harus identik, tetapi membuatnya tetap identik akan mengurangi error. Oleh karena itu, dalam codelab ini, kita juga akan menggunakan ID yang identik.
3. Menginstal plugin
Di bagian codelab ini, Anda akan menginstal plugin in_app_purchase.
Menambahkan dependensi di pubspec
Tambahkan in_app_purchase
ke pubspec dengan menambahkan in_app_purchase
ke dependensi di pubspec Anda:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Buka pubspec.yaml
, dan konfirmasi bahwa Anda sekarang memiliki in_app_purchase
yang tercantum sebagai entri di bagian dependencies
, dan in_app_purchase_platform_interface
di bagian 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
Klik pub get untuk mendownload paket atau jalankan flutter pub get
di command line.
4. Menyiapkan App Store
Untuk menyiapkan pembelian dalam aplikasi dan mengujinya di iOS, Anda perlu membuat aplikasi baru di App Store dan membuat produk yang dapat dibeli di sana. Anda tidak perlu memublikasikan apa pun atau mengirimkan aplikasi ke Apple untuk ditinjau. Anda memerlukan akun developer untuk melakukannya. Jika Anda belum memilikinya, daftar ke program developer Apple.
Perjanjian Aplikasi Berbayar
Untuk menggunakan pembelian dalam aplikasi, Anda juga harus memiliki perjanjian aktif untuk aplikasi berbayar di App Store Connect. Buka https://appstoreconnect.apple.com/, lalu klik Perjanjian, Pajak, dan Perbankan.
Anda akan melihat perjanjian di sini untuk aplikasi gratis dan berbayar. Status aplikasi gratis harus aktif, dan status untuk aplikasi berbayar adalah baru. Pastikan Anda melihat persyaratan, menyetujuinya, dan memasukkan semua informasi yang diperlukan.
Jika semuanya ditetapkan dengan benar, status untuk aplikasi berbayar akan aktif. Hal ini sangat penting karena Anda tidak akan dapat mencoba pembelian dalam aplikasi tanpa perjanjian yang aktif.
Mendaftarkan ID Aplikasi
Buat ID baru di portal developer Apple.
Memilih ID Aplikasi
Pilih Aplikasi
Berikan beberapa deskripsi dan tetapkan ID paket agar cocok dengan ID paket ke nilai yang sama seperti yang ditetapkan sebelumnya di XCode.
Untuk panduan selengkapnya tentang cara membuat ID aplikasi baru, lihat Bantuan Akun Developer .
Membuat aplikasi baru
Buat aplikasi baru di App Store Connect dengan ID paket unik Anda.
Untuk panduan selengkapnya tentang cara membuat aplikasi baru dan mengelola perjanjian, lihat bantuan App Store Connect.
Untuk menguji pembelian dalam aplikasi, Anda memerlukan pengguna pengujian sandbox. Pengguna pengujian ini tidak boleh terhubung ke iTunes—pengguna ini hanya digunakan untuk menguji pembelian dalam aplikasi. Anda tidak dapat menggunakan alamat email yang sudah digunakan untuk akun Apple. Di Pengguna dan Akses, buka Penguji di bagian Sandbox untuk membuat akun sandbox baru atau mengelola Apple ID sandbox yang ada.
Sekarang Anda dapat menyiapkan pengguna sandbox di iPhone dengan membuka Setelan > App Store > Sandbox-account.
Mengonfigurasi pembelian dalam aplikasi
Sekarang Anda akan mengonfigurasi tiga item yang dapat dibeli:
dash_consumable_2k
: Pembelian habis pakai yang dapat dibeli berkali-kali, yang memberi pengguna 2.000 Dash (mata uang dalam aplikasi) per pembelian.dash_upgrade_3d
: Pembelian "upgrade" non-konsumsi yang hanya dapat dibeli satu kali, dan memberi pengguna Dash yang berbeda secara kosmetik untuk diklik.dash_subscription_doubler
: Langganan yang memberi pengguna dua kali lebih banyak Dash per klik selama durasi langganan.
Buka Pembelian Dalam Aplikasi > Kelola.
Buat pembelian dalam aplikasi dengan ID yang ditentukan:
- Siapkan
dash_consumable_2k
sebagai Konsumsi.
Gunakan dash_consumable_2k
sebagai ID Produk. Nama referensi hanya digunakan di App Store Connect, cukup tetapkan ke dash consumable 2k
dan tambahkan pelokalan Anda untuk pembelian. Panggil pembelian Spring is in the air
dengan 2000 dashes fly out
sebagai deskripsi.
- Siapkan
dash_upgrade_3d
sebagai Tidak habis pakai.
Gunakan dash_upgrade_3d
sebagai ID Produk. Tetapkan nama referensi ke dash upgrade 3d
dan tambahkan pelokalan Anda untuk pembelian. Panggil pembelian 3D Dash
dengan Brings your dash back to the future
sebagai deskripsi.
- Siapkan
dash_subscription_doubler
sebagai Langganan yang diperpanjang otomatis.
Alur untuk langganan sedikit berbeda. Pertama, Anda harus menetapkan Nama Referensi dan ID Produk:
Selanjutnya, Anda harus membuat grup langganan. Jika beberapa langganan merupakan bagian dari grup yang sama, pengguna hanya dapat berlangganan salah satu langganan tersebut secara bersamaan, tetapi dapat dengan mudah mengupgrade atau mendowngrade di antara langganan tersebut. Cukup beri nama grup ini subscriptions
.
Selanjutnya, masukkan durasi langganan dan pelokalan. Beri nama langganan ini Jet Engine
dengan deskripsi Doubles your clicks
. Klik Simpan.
Setelah mengklik tombol Simpan, tambahkan harga langganan. Pilih harga yang Anda inginkan.
Sekarang Anda akan melihat tiga pembelian dalam daftar pembelian:
5. Menyiapkan Play Store
Seperti halnya App Store, Anda juga memerlukan akun developer untuk Play Store. Jika belum memilikinya, daftarkan akun.
Membuat aplikasi baru
Buat aplikasi baru di Konsol Google Play:
- Buka Konsol Play.
- Pilih Semua aplikasi > Buat aplikasi.
- Pilih bahasa default, lalu tambahkan judul untuk aplikasi Anda. Ketik nama aplikasi yang nantinya muncul di Google Play sesuai keinginan Anda. Anda dapat mengubah nama tersebut di lain waktu.
- Tentukan bahwa aplikasi Anda adalah game. Anda dapat mengubahnya nanti.
- Tentukan apakah aplikasi Anda gratis atau berbayar.
- Tambahkan alamat email yang dapat digunakan pengguna Play Store untuk menghubungi Anda terkait aplikasi ini.
- Lengkapi pernyataan Panduan konten dan hukum ekspor Amerika Serikat.
- Pilih Buat aplikasi.
Setelah aplikasi dibuat, buka dasbor, lalu selesaikan semua tugas di bagian Siapkan aplikasi Anda. Di sini, Anda memberikan beberapa informasi tentang aplikasi, seperti rating konten dan screenshot.
Menandatangani aplikasi
Agar dapat menguji pembelian dalam aplikasi, Anda memerlukan minimal satu build yang diupload ke Google Play.
Untuk melakukannya, Anda perlu menandatangani build rilis dengan sesuatu selain kunci debug.
Membuat keystore
Jika Anda sudah memiliki keystore, lanjutkan ke langkah berikutnya. Jika belum, buat bucket dengan menjalankan perintah berikut di command line.
Di Mac/Linux, gunakan perintah berikut:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
Di Windows, gunakan perintah berikut:
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
Perintah ini menyimpan file key.jks
di direktori beranda Anda. Jika Anda ingin menyimpan file di tempat lain, ubah argumen yang Anda teruskan ke parameter -keystore
. Simpan
keystore
file pribadi; jangan periksa ke dalam kontrol sumber publik.
Mereferensikan keystore dari aplikasi
Buat file bernama <your app dir>/android/key.properties
yang berisi referensi ke keystore Anda:
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>
Mengonfigurasi penandatanganan di Gradle
Konfigurasikan penandatanganan untuk aplikasi Anda dengan mengedit file <your app dir>/android/app/build.gradle
.
Tambahkan informasi keystore dari file properti Anda sebelum blok android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Muat file key.properties
ke objek keystoreProperties
.
Tambahkan kode berikut sebelum blok 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
}
}
Konfigurasikan blok signingConfigs
dalam file build.gradle
modul Anda dengan informasi konfigurasi penandatanganan:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
Build rilis aplikasi Anda kini akan ditandatangani secara otomatis.
Untuk mengetahui informasi selengkapnya tentang menandatangani aplikasi, lihat Menandatangani aplikasi di developer.android.com.
Mengupload build pertama Anda
Setelah aplikasi dikonfigurasi untuk penandatanganan, Anda akan dapat mem-build aplikasi dengan menjalankan:
flutter build appbundle
Perintah ini menghasilkan build rilis secara default dan output-nya dapat ditemukan di <your app dir>/build/app/outputs/bundle/release/
Dari dasbor di Konsol Google Play, buka Rilis > Pengujian > Pengujian tertutup, lalu buat rilis pengujian tertutup baru.
Untuk codelab ini, Anda akan tetap menggunakan Google untuk menandatangani aplikasi. Jadi, lanjutkan dan tekan Lanjutkan di bagian Penandatanganan Aplikasi Play untuk ikut serta.
Selanjutnya, upload app bundle app-release.aab
yang dihasilkan oleh perintah build.
Klik Simpan, lalu klik Tinjau rilis.
Terakhir, klik Start rollout to Internal testing untuk mengaktifkan rilis pengujian internal.
Menyiapkan pengguna pengujian
Agar dapat menguji pembelian dalam aplikasi, akun Google penguji Anda harus ditambahkan di konsol Google Play di dua lokasi:
- Ke jalur pengujian tertentu (Pengujian internal)
- Sebagai penguji lisensi
Pertama, mulai dengan menambahkan penguji ke jalur pengujian internal. Kembali ke Rilis > Pengujian > Pengujian internal, lalu klik tab Penguji.
Buat daftar email baru dengan mengklik Buat daftar email. Beri nama daftar, lalu tambahkan alamat email Akun Google yang memerlukan akses untuk menguji pembelian dalam aplikasi.
Selanjutnya, centang kotak untuk daftar, lalu klik Simpan perubahan.
Kemudian, tambahkan penguji lisensi:
- Kembali ke tampilan Semua aplikasi di Konsol Google Play.
- Buka Setelan > Pengujian lisensi.
- Tambahkan alamat email yang sama dari penguji yang perlu dapat menguji pembelian dalam aplikasi.
- Tetapkan License response ke
RESPOND_NORMALLY
. - Klik Simpan perubahan.
Mengonfigurasi pembelian dalam aplikasi
Sekarang Anda akan mengonfigurasi item yang dapat dibeli dalam aplikasi.
Sama seperti di App Store, Anda harus menentukan tiga pembelian yang berbeda:
dash_consumable_2k
: Pembelian habis pakai yang dapat dibeli berkali-kali, yang memberi pengguna 2.000 Dash (mata uang dalam aplikasi) per pembelian.dash_upgrade_3d
: Pembelian "upgrade" yang tidak dapat digunakan dan hanya dapat dibeli satu kali, yang memberi pengguna Dash yang berbeda secara kosmetik untuk diklik.dash_subscription_doubler
: Langganan yang memberi pengguna dua kali lebih banyak Dash per klik selama durasi langganan.
Pertama, tambahkan item habis pakai dan tidak habis pakai.
- Buka Konsol Google Play, lalu pilih aplikasi Anda.
- Buka Monetisasi > Produk > Produk dalam aplikasi.
- Klik Buat produk
- Masukkan semua informasi yang diperlukan untuk produk Anda. Pastikan ID produk sama persis dengan ID yang ingin Anda gunakan.
- Klik Simpan.
- Klik Aktifkan.
- Ulangi proses untuk pembelian "upgrade" yang tidak habis pakai.
Selanjutnya, tambahkan langganan:
- Buka Konsol Google Play, lalu pilih aplikasi Anda.
- Buka Monetisasi > Produk > Langganan.
- Klik Buat langganan
- Masukkan semua informasi yang diperlukan untuk langganan Anda. Pastikan ID produk sama persis dengan ID yang ingin Anda gunakan.
- Klik Simpan
Pembelian Anda kini akan disiapkan di Konsol Play.
6. Menyiapkan Firebase
Dalam codelab ini, Anda akan menggunakan layanan backend untuk memverifikasi dan melacak pembelian pengguna.
Penggunaan layanan backend memiliki beberapa manfaat:
- Anda dapat memverifikasi transaksi dengan aman.
- Anda dapat merespons peristiwa penagihan dari app store.
- Anda dapat melacak pembelian di database.
- Pengguna tidak akan dapat mengelabui aplikasi Anda untuk menyediakan fitur premium dengan memundurkan jam sistem mereka.
Meskipun ada banyak cara untuk menyiapkan layanan backend, Anda akan melakukannya menggunakan cloud function dan Firestore, menggunakan Firebase milik Google sendiri.
Menulis backend dianggap di luar cakupan untuk codelab ini, sehingga kode awal sudah menyertakan project Firebase yang menangani pembelian dasar untuk memulai.
Plugin Firebase juga disertakan dengan aplikasi awal.
Yang perlu Anda lakukan adalah membuat project Firebase Anda sendiri, mengonfigurasi aplikasi dan backend untuk Firebase, dan terakhir men-deploy backend.
Membuat project Firebase
Buka Firebase console, lalu buat project Firebase baru. Untuk contoh ini, beri nama project Dash Clicker.
Di aplikasi backend, Anda mengaitkan pembelian ke pengguna tertentu, sehingga Anda memerlukan autentikasi. Untuk melakukannya, manfaatkan modul autentikasi Firebase dengan login dengan Google.
- Dari dasbor Firebase, buka Authentication dan aktifkan, jika diperlukan.
- Buka tab Metode login, lalu aktifkan penyedia login Google.
Karena Anda juga akan menggunakan database Firestore Firebase, aktifkan juga opsi ini.
Tetapkan aturan Cloud Firestore seperti ini:
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
}
}
}
Menyiapkan Firebase untuk Flutter
Cara yang direkomendasikan untuk menginstal Firebase di aplikasi Flutter adalah menggunakan FlutterFire CLI. Ikuti petunjuk seperti yang dijelaskan di halaman penyiapan.
Saat menjalankan flutterfire configure, pilih project yang baru saja Anda buat di langkah sebelumnya.
$ 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>
Selanjutnya, aktifkan iOS dan Android dengan memilih kedua platform tersebut.
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
Saat diminta untuk mengganti firebase_options.dart, pilih yes.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Menyiapkan Firebase untuk Android: Langkah lebih lanjut
Dari dasbor Firebase, buka Ringkasan Project, pilih Setelan, lalu pilih tab Umum.
Scroll ke bawah ke Aplikasi Anda, lalu pilih aplikasi dashclicker (android).
Untuk mengizinkan login dengan Google dalam mode debug, Anda harus memberikan sidik jari hash SHA-1 dari sertifikat debug Anda.
Mendapatkan hash sertifikat penandatanganan debug
Di root project aplikasi Flutter, ubah direktori ke folder android/
, lalu buat laporan penandatanganan.
cd android ./gradlew :app:signingReport
Anda akan melihat daftar besar kunci penandatanganan. Karena Anda mencari hash untuk sertifikat debug, cari sertifikat dengan properti Variant
dan Config
yang ditetapkan ke debug
. Keystore kemungkinan berada di folder beranda Anda di bagian .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
Salin hash SHA-1, lalu isi kolom terakhir di dialog modal pengiriman aplikasi.
Menyiapkan Firebase untuk iOS: Langkah lebih lanjut
Buka ios/Runnder.xcworkspace
dengan Xcode
. Atau dengan IDE pilihan Anda.
Di VSCode, klik kanan folder ios/
, lalu open in xcode
.
Di Android Studio, klik kanan folder ios/
, lalu klik flutter
, diikuti dengan opsi open iOS module in Xcode
.
Untuk mengizinkan login dengan Google di iOS, tambahkan opsi konfigurasi CFBundleURLTypes
ke file plist
build Anda. (Lihat dokumen paket google_sign_in
untuk informasi selengkapnya.) Dalam hal ini, file-nya adalah ios/Runner/Info-Debug.plist
dan ios/Runner/Info-Release.plist
.
Pasangan nilai kunci sudah ditambahkan, tetapi nilainya harus diganti:
- Dapatkan nilai untuk
REVERSED_CLIENT_ID
dari fileGoogleService-Info.plist
, tanpa elemen<string>..</string>
yang mengelilinginya. - Ganti nilai di file
ios/Runner/Info-Debug.plist
danios/Runner/Info-Release.plist
Anda di bagian kunciCFBundleURLTypes
.
<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>
Sekarang Anda telah menyelesaikan penyiapan Firebase.
7. Memproses pembaruan pembelian
Di bagian codelab ini, Anda akan menyiapkan aplikasi untuk membeli produk. Proses ini mencakup pemrosesan update dan error pembelian setelah aplikasi dimulai.
Mendengarkan update pembelian
Di main.dart,
, temukan widget MyHomePage
yang memiliki Scaffold
dengan BottomNavigationBar
yang berisi dua halaman. Halaman ini juga membuat tiga Provider
untuk DashCounter
, DashUpgrades,
, dan DashPurchases
. DashCounter
melacak jumlah Tanda Hubung saat ini dan menambahkannya secara otomatis. DashUpgrades
mengelola upgrade yang dapat Anda beli dengan Dash. Codelab ini berfokus pada DashPurchases
.
Secara default, objek penyedia ditentukan saat objek tersebut pertama kali diminta. Objek ini memproses pembaruan pembelian secara langsung saat aplikasi dimulai, jadi nonaktifkan pemuatan lambat pada objek ini dengan lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
Anda juga memerlukan instance InAppPurchaseConnection
. Namun, agar aplikasi tetap dapat diuji, Anda memerlukan beberapa cara untuk meniru koneksi. Untuk melakukannya, buat metode instance yang dapat diganti dalam pengujian, lalu tambahkan ke 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!;
}
}
Anda harus sedikit memperbarui pengujian jika ingin pengujian terus berfungsi.
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.
Di lib/logic/dash_purchases.dart
, buka kode untuk DashPurchases ChangeNotifier
. Saat ini, hanya ada DashCounter
yang dapat Anda tambahkan ke Dash yang dibeli.
Tambahkan properti langganan streaming, _subscription
(jenis StreamSubscription<List<PurchaseDetails>> _subscription;
), IAPConnection.instance,
, dan impor. Kode yang dihasilkan akan terlihat seperti berikut:
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();
}
}
Kata kunci late
ditambahkan ke _subscription
karena _subscription
diinisialisasi di konstruktor. Project ini disiapkan agar tidak nullable secara default (NNBD), yang berarti bahwa properti yang tidak dideklarasikan nullable harus memiliki nilai non-null. Penentu late
memungkinkan Anda menunda penentuan nilai ini.
Di konstruktor, dapatkan streaming purchaseUpdated
dan mulai memproses streaming. Dalam metode dispose()
, batalkan langganan streaming.
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.
}
Sekarang, aplikasi menerima pembaruan pembelian sehingga, di bagian berikutnya, Anda akan melakukan pembelian.
Sebelum melanjutkan, jalankan pengujian dengan "flutter test"
untuk memverifikasi bahwa semuanya telah disiapkan dengan benar.
$ flutter test
00:01 +1: All tests passed!
8. Melakukan pembelian
Di bagian codelab ini, Anda akan mengganti produk tiruan yang ada saat ini dengan produk nyata yang dapat dibeli. Produk ini dimuat dari toko, ditampilkan dalam daftar, dan dibeli saat mengetuk produk.
Menyesuaikan PurchasableProduct
PurchasableProduct
menampilkan produk tiruan. Perbarui untuk menampilkan konten sebenarnya dengan mengganti class PurchasableProduct
di purchasable_product.dart
dengan kode berikut:
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;
}
Di dash_purchases.dart,
, hapus pembelian dummy dan ganti dengan daftar kosong, List<PurchasableProduct> products = [];
Memuat pembelian yang tersedia
Untuk memberi pengguna kemampuan melakukan pembelian, muat pembelian dari toko. Pertama, periksa apakah toko tersedia. Jika toko tidak tersedia, menyetel storeState
ke notAvailable
akan menampilkan pesan error kepada pengguna.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
Jika toko tersedia, muat pembelian yang tersedia. Dengan penyiapan Firebase sebelumnya, Anda akan melihat storeKeyConsumable
, storeKeySubscription,
, dan storeKeyUpgrade
. Jika pembelian yang diharapkan tidak tersedia, cetak informasi ini ke konsol; Anda juga dapat mengirim info ini ke layanan backend.
Metode await iapConnection.queryProductDetails(ids)
menampilkan ID yang tidak ditemukan dan produk yang dapat dibeli yang ditemukan. Gunakan productDetails
dari respons untuk memperbarui UI, dan tetapkan StoreState
ke 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();
}
Panggil fungsi loadPurchases()
di konstruktor:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
Terakhir, ubah nilai kolom storeState
dari StoreState.available
menjadi StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Menampilkan produk yang dapat dibeli
Pertimbangkan file purchase_page.dart
. Widget PurchasePage
menampilkan _PurchasesLoading
, _PurchaseList,
, atau _PurchasesNotAvailable,
, bergantung pada StoreState
. Widget ini juga menampilkan pembelian sebelumnya dari pengguna yang digunakan di langkah berikutnya.
Widget _PurchaseList
menampilkan daftar produk yang dapat dibeli dan mengirim permintaan pembelian ke objek 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(),
);
}
}
Anda akan dapat melihat produk yang tersedia di Play Store dan App Store Android dan iOS jika produk tersebut dikonfigurasi dengan benar. Perhatikan bahwa mungkin perlu beberapa saat sebelum pembelian tersedia saat dimasukkan ke konsol masing-masing.
Kembali ke dash_purchases.dart
, dan terapkan fungsi untuk membeli produk. Anda hanya perlu memisahkan barang habis pakai dari barang yang tidak habis pakai. Produk upgrade dan langganan tidak habis pakai.
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');
}
}
Sebelum melanjutkan, buat variabel _beautifiedDashUpgrade
dan perbarui pengambil beautifiedDash
untuk mereferensikannya.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
Metode _onPurchaseUpdate
menerima pembaruan pembelian, memperbarui status produk yang ditampilkan di halaman pembelian, dan menerapkan pembelian ke logika penghitung. Anda harus memanggil completePurchase
setelah menangani pembelian agar toko mengetahui bahwa pembelian ditangani dengan benar.
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. Siapkan backend
Sebelum melanjutkan ke pelacakan dan verifikasi pembelian, siapkan backend Dart untuk mendukungnya.
Di bagian ini, bekerjalah dari folder dart-backend/
sebagai root.
Pastikan Anda telah menginstal alat berikut:
- Dart
- Firebase CLI
Ringkasan project dasar
Karena beberapa bagian project ini dianggap di luar cakupan untuk codelab ini, bagian tersebut disertakan dalam kode awal. Sebaiknya tinjau apa yang sudah ada dalam kode awal sebelum memulai, untuk mendapatkan gambaran tentang cara Anda menyusun struktur.
Kode backend ini dapat berjalan secara lokal di komputer Anda, Anda tidak perlu men-deploynya untuk menggunakannya. Namun, Anda harus dapat terhubung dari perangkat pengembangan (Android atau iPhone) ke mesin tempat server akan berjalan. Untuk itu, keduanya harus berada di jaringan yang sama, dan Anda perlu mengetahui alamat IP komputer.
Coba jalankan server menggunakan perintah berikut:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
Backend Dart menggunakan shelf
dan shelf_router
untuk menayangkan endpoint API. Secara default, server tidak menyediakan rute apa pun. Nanti, Anda akan membuat rute untuk menangani proses verifikasi pembelian.
Satu bagian yang sudah disertakan dalam kode awal adalah IapRepository
di lib/iap_repository.dart
. Karena mempelajari cara berinteraksi dengan Firestore, atau database secara umum, tidak dianggap relevan dengan codelab ini, kode awal berisi fungsi untuk membuat atau memperbarui pembelian di Firestore, serta semua class untuk pembelian tersebut.
Menyiapkan akses Firebase
Untuk mengakses Firebase Firestore, Anda memerlukan kunci akses akun layanan. Buat kunci dengan membuka setelan project Firebase dan membuka bagian Service accounts, lalu pilih Generate new private key.
Salin file JSON yang didownload ke folder assets/
, lalu ganti namanya menjadi service-account-firebase.json
.
Menyiapkan akses Google Play
Untuk mengakses Play Store guna memverifikasi pembelian, Anda harus membuat akun layanan dengan izin ini, dan mendownload kredensial JSON untuk akun tersebut.
- Buka Konsol Google Play, lalu mulai dari halaman Semua aplikasi.
- Buka Penyiapan > Akses API.
Jika Konsol Google Play meminta Anda membuat atau menautkan ke project yang ada, lakukan hal tersebut terlebih dahulu, lalu kembali ke halaman ini.
- Temukan bagian tempat Anda dapat menentukan akun layanan, lalu klik Buat akun layanan baru.
- Klik link Google Cloud Platform di dialog yang muncul.
- Pilih project Anda. Jika tidak melihatnya, pastikan Anda login ke Akun Google yang benar di bagian daftar drop-down Akun di kanan atas.
- Setelah memilih project, klik + Buat Akun Layanan di panel menu atas.
- Berikan nama untuk akun layanan, berikan deskripsi (opsional) agar Anda dapat mengingat tujuannya, lalu lanjutkan ke langkah berikutnya.
- Tetapkan peran Editor ke akun layanan.
- Selesaikan wizard, kembali ke halaman Akses API dalam konsol developer, lalu klik Muat ulang akun layanan. Anda akan melihat akun yang baru dibuat dalam daftar.
- Klik Berikan akses untuk akun layanan baru Anda.
- Scroll ke bawah halaman berikutnya, ke blok Data keuangan. Pilih Melihat data keuangan, pesanan, dan respons survei pembatalan serta Mengelola pesanan dan langganan.
- Klik Undang pengguna.
- Setelah akun disiapkan, Anda hanya perlu membuat beberapa kredensial. Kembali ke Cloud Console, temukan akun layanan Anda dalam daftar akun layanan, klik tiga titik vertikal, lalu pilih Kelola kunci.
- Buat kunci JSON baru dan download.
- Ganti nama file yang didownload menjadi
service-account-google-play.json,
dan pindahkan ke direktoriassets/
.
Satu hal lagi yang perlu kita lakukan adalah membuka lib/constants.dart,
dan mengganti nilai androidPackageId
dengan ID paket yang Anda pilih untuk aplikasi Android.
Menyiapkan akses Apple App Store
Untuk mengakses App Store guna memverifikasi pembelian, Anda harus menyiapkan secret bersama:
- Buka App Store Connect.
- Buka Aplikasi Saya,lalu pilih aplikasi Anda.
- Di navigasi sidebar, buka Pembelian dalam Aplikasi > Kelola.
- Di kanan atas daftar, klik Rahasia Bersama Khusus Aplikasi.
- Buat secret baru, lalu salin.
- Buka
lib/constants.dart,
dan ganti nilaiappStoreSharedSecret
dengan secret bersama yang baru saja Anda buat.
File konfigurasi konstanta
Sebelum melanjutkan, pastikan konstanta berikut dikonfigurasi dalam file lib/constants.dart
:
androidPackageId
: ID Paket yang digunakan di Android. misalnya,com.example.dashclicker
appStoreSharedSecret
: Rahasia bersama untuk mengakses App Store Connect guna melakukan verifikasi pembelian.bundleId
: ID paket yang digunakan di iOS. misalnya,com.example.dashclicker
Untuk saat ini, Anda dapat mengabaikan konstanta lainnya.
10. Memverifikasi pembelian
Alur umum untuk memverifikasi pembelian serupa untuk iOS dan Android.
Untuk kedua toko tersebut, aplikasi Anda akan menerima token saat pembelian dilakukan.
Token ini dikirim oleh aplikasi ke layanan backend Anda, yang kemudian memverifikasi pembelian dengan server toko masing-masing menggunakan token yang diberikan.
Layanan backend kemudian dapat memilih untuk menyimpan pembelian, dan membalas aplikasi apakah pembelian tersebut valid atau tidak.
Dengan meminta layanan backend melakukan validasi dengan toko, bukan aplikasi yang berjalan di perangkat pengguna, Anda dapat mencegah pengguna mendapatkan akses ke fitur premium dengan, misalnya, memundurkan jam sistem mereka.
Menyiapkan sisi Flutter
Menyiapkan autentikasi
Karena Anda akan mengirim pembelian ke layanan backend, Anda ingin memastikan pengguna diautentikasi saat melakukan pembelian. Sebagian besar logika autentikasi sudah ditambahkan untuk Anda di project awal, Anda hanya perlu memastikan PurchasePage
menampilkan tombol login saat pengguna belum login. Tambahkan kode berikut ke awal metode 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
Memanggil endpoint verifikasi dari aplikasi
Di aplikasi, buat fungsi _verifyPurchase(PurchaseDetails purchaseDetails)
yang memanggil endpoint /verifypurchase
di backend Dart menggunakan panggilan post http.
Kirim toko yang dipilih (google_play
untuk Play Store atau app_store
untuk App Store), serverVerificationData
, dan productID
. Server menampilkan kode status yang menunjukkan apakah pembelian diverifikasi.
Di konstanta aplikasi, konfigurasikan IP server ke alamat IP komputer lokal Anda.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
Tambahkan firebaseNotifier
dengan pembuatan DashPurchases
di main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
Tambahkan pengambil untuk Pengguna di FirebaseNotifier, sehingga Anda dapat meneruskan ID pengguna ke fungsi verifikasi pembelian.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
Tambahkan fungsi _verifyPurchase
ke class DashPurchases
. Fungsi async
ini menampilkan boolean yang menunjukkan apakah pembelian divalidasi.
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;
}
}
Panggil fungsi _verifyPurchase
di _handlePurchase
tepat sebelum Anda menerapkan pembelian. Anda hanya boleh menerapkan pembelian jika sudah diverifikasi. Di aplikasi produksi, Anda dapat menentukannya lebih lanjut, misalnya, menerapkan langganan uji coba saat toko tidak tersedia untuk sementara. Namun, untuk contoh ini, buatlah sederhana, dan hanya terapkan pembelian jika pembelian berhasil diverifikasi.
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);
}
}
Di aplikasi, semuanya kini siap untuk memvalidasi pembelian.
Menyiapkan layanan backend
Selanjutnya, siapkan fungsi cloud untuk memverifikasi pembelian di backend.
Mem-build pengendali pembelian
Karena alur verifikasi untuk kedua toko hampir identik, siapkan class PurchaseHandler
abstrak dengan implementasi terpisah untuk setiap toko.
Mulai dengan menambahkan file purchase_handler.dart
ke folder lib/
, tempat Anda menentukan class PurchaseHandler
abstrak dengan dua metode abstrak untuk memverifikasi dua jenis pembelian yang berbeda: langganan dan non-langganan.
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,
});
}
Seperti yang dapat Anda lihat, setiap metode memerlukan tiga parameter:
userId:
ID pengguna yang login, sehingga Anda dapat mengaitkan pembelian dengan pengguna.productData:
Data tentang produk. Anda akan menentukannya dalam beberapa menit.token:
Token yang diberikan kepada pengguna oleh Play Store.
Selain itu, agar pengendali pembelian ini lebih mudah digunakan, tambahkan metode verifyPurchase()
yang dapat digunakan untuk langganan dan non-langganan:
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,
);
}
}
Sekarang, Anda cukup memanggil verifyPurchase
untuk kedua kasus tersebut, tetapi tetap memiliki implementasi terpisah.
Class ProductData
berisi informasi dasar tentang berbagai produk yang dapat dibeli, yang mencakup ID produk (terkadang juga disebut sebagai SKU) dan ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ProductType
dapat berupa langganan atau non-langganan.
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
Terakhir, daftar produk ditentukan sebagai peta dalam file yang sama.
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,
),
};
Selanjutnya, tentukan beberapa implementasi placeholder untuk Google Play Store dan Apple App Store. Mulai dengan Google Play:
Buat lib/google_play_purchase_handler.dart
, dan tambahkan class yang memperluas PurchaseHandler
yang baru saja Anda tulis:
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;
}
}
Untuk saat ini, metode ini menampilkan true
untuk metode pengendali; Anda akan melihatnya nanti.
Seperti yang mungkin Anda perhatikan, konstruktor mengambil instance IapRepository
. Pengendali pembelian menggunakan instance ini untuk menyimpan informasi tentang pembelian di Firestore nanti. Untuk berkomunikasi dengan Google Play, Anda menggunakan AndroidPublisherApi
yang disediakan.
Selanjutnya, lakukan hal yang sama untuk pengendali app store. Buat lib/app_store_purchase_handler.dart
, dan tambahkan class yang memperluas PurchaseHandler
lagi:
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;
}
}
Bagus! Sekarang Anda memiliki dua pengendali pembelian. Selanjutnya, mari kita buat endpoint API verifikasi pembelian.
Menggunakan pengendali pembelian
Buka bin/server.dart
dan buat endpoint API menggunakan 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');
}
}
Kode di atas melakukan hal berikut:
- Tentukan endpoint POST yang akan dipanggil dari aplikasi yang Anda buat sebelumnya.
- Dekode payload JSON dan ekstrak informasi berikut:
userId
: ID pengguna yang saat ini loginsource
: Penyimpanan yang digunakan,app_store
ataugoogle_play
.productData
: Diperoleh dariproductDataMap
yang Anda buat sebelumnya.token
: Berisi data verifikasi yang akan dikirim ke toko.- Panggilan ke metode
verifyPurchase
, baik untukGooglePlayPurchaseHandler
maupunAppStorePurchaseHandler
, bergantung pada sumbernya. - Jika verifikasi berhasil, metode akan menampilkan
Response.ok
ke klien. - Jika verifikasi gagal, metode akan menampilkan
Response.internalServerError
ke klien.
Setelah membuat endpoint API, Anda perlu mengonfigurasi dua pengendali pembelian. Untuk melakukannya, Anda harus memuat kunci akun layanan yang diperoleh di langkah sebelumnya dan mengonfigurasi akses ke berbagai layanan, termasuk Android Publisher API dan Firebase Firestore API. Kemudian, buat dua pengendali pembelian dengan dependensi yang berbeda:
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,
),
};
}
Memverifikasi pembelian Android: Mengimplementasikan pengendali pembelian
Selanjutnya, lanjutkan menerapkan pengendali pembelian Google Play.
Google sudah menyediakan paket Dart untuk berinteraksi dengan API yang Anda perlukan untuk memverifikasi pembelian. Anda melakukan inisialisasi di file server.dart
dan sekarang menggunakannya di class GooglePlayPurchaseHandler
.
Terapkan pengendali untuk pembelian jenis non-langganan:
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;
}
Anda dapat memperbarui pengendali pembelian langganan dengan cara yang serupa:
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;
}
}
Tambahkan metode berikut untuk memfasilitasi penguraian ID pesanan, serta dua metode untuk mengurai status pembelian.
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;
}
Pembelian Google Play Anda kini akan diverifikasi dan disimpan dalam database.
Selanjutnya, lanjutkan ke pembelian App Store untuk iOS.
Memverifikasi pembelian iOS: Mengimplementasikan pengendali pembelian
Untuk memverifikasi pembelian dengan App Store, ada paket Dart pihak ketiga bernama app_store_server_sdk
yang mempermudah prosesnya.
Mulai dengan membuat instance ITunesApi
. Gunakan konfigurasi sandbox, serta aktifkan logging untuk memfasilitasi proses debug error.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
Sekarang, tidak seperti Google Play API, App Store menggunakan endpoint API yang sama untuk langganan dan non-langganan. Artinya, Anda dapat menggunakan logika yang sama untuk kedua pengendali. Gabungkan keduanya sehingga memanggil implementasi yang sama:
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 {
//..
}
Sekarang, terapkan 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;
}
}
Pembelian App Store Anda kini akan diverifikasi dan disimpan di database.
Menjalankan backend
Pada tahap ini, Anda dapat menjalankan dart bin/server.dart
untuk menayangkan endpoint /verifypurchase
.
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. Melacak pembelian
Cara yang direkomendasikan untuk melacak pembelian pengguna Anda adalah di layanan backend. Hal ini karena backend Anda dapat merespons peristiwa dari toko sehingga tidak rentan mengalami informasi yang sudah tidak berlaku karena caching, serta tidak rentan terhadap modifikasi tidak sah.
Pertama, siapkan pemrosesan peristiwa toko di backend dengan backend Dart yang telah Anda buat.
Memproses peristiwa toko di backend
Toko memiliki kemampuan untuk memberi tahu backend Anda tentang peristiwa penagihan yang terjadi, seperti saat langganan diperpanjang. Anda dapat memproses peristiwa ini di backend untuk memastikan pembelian di database Anda selalu terbaru. Di bagian ini, siapkan untuk Google Play Store dan Apple App Store.
Memproses peristiwa penagihan Google Play
Google Play menyediakan peristiwa penagihan melalui apa yang mereka sebut topik cloud pub/sub. Ini pada dasarnya adalah antrean pesan tempat pesan dapat dipublikasikan, serta digunakan.
Karena ini adalah fungsi khusus untuk Google Play, Anda menyertakan fungsi ini dalam GooglePlayPurchaseHandler
.
Mulai dengan membuka lib/google_play_purchase_handler.dart
, dan menambahkan impor PubsubApi:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Kemudian, teruskan PubsubApi
ke GooglePlayPurchaseHandler
, dan ubah konstruktor class untuk membuat Timer
sebagai berikut:
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
dikonfigurasi untuk memanggil metode _pullMessageFromSubSub
setiap sepuluh detik. Anda dapat menyesuaikan Durasi sesuai preferensi Anda sendiri.
Kemudian, buat _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,
);
}
Kode yang baru saja Anda tambahkan berkomunikasi dengan Topik Pub/Sub dari Google Cloud setiap sepuluh detik dan meminta pesan baru. Kemudian, memproses setiap pesan dalam metode _processMessage
.
Metode ini mendekode pesan yang masuk dan mendapatkan informasi terbaru tentang setiap pembelian, baik langganan maupun non-langganan, dengan memanggil handleSubscription
atau handleNonSubscription
yang ada jika diperlukan.
Setiap pesan harus dikonfirmasi dengan metode _askMessage
.
Selanjutnya, tambahkan dependensi yang diperlukan ke file server.dart
. Tambahkan PubsubApi.cloudPlatformScope ke konfigurasi kredensial:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
Kemudian, buat instance PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Terakhir, teruskan ke konstruktor GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Penyiapan Google Play
Anda telah menulis kode untuk menggunakan peristiwa penagihan dari topik pub/sub, tetapi belum membuat topik pub/sub, atau memublikasikan peristiwa penagihan apa pun. Sekarang saatnya menyiapkannya.
Pertama, buat topik pub/sub:
- Buka halaman Cloud Pub/Sub di Google Cloud Console.
- Pastikan Anda berada di project Firebase, lalu klik + Buat Topik.
- Berikan nama untuk topik baru, yang identik dengan nilai yang ditetapkan untuk
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
diconstants.ts
. Dalam hal ini, beri namaplay_billing
. Jika Anda memilih yang lain, pastikan untuk memperbaruiconstants.ts
. Buat topik. - Di daftar topik pub/sub, klik tiga titik vertikal untuk topik yang baru saja Anda buat, lalu klik Lihat izin.
- Di sidebar sebelah kanan, pilih Tambahkan akun utama.
- Di sini, tambahkan
google-play-developer-notifications@system.gserviceaccount.com
, lalu berikan peran Pub/Sub Publisher. - Simpan perubahan izin.
- Salin Nama topik topik yang baru saja Anda buat.
- Buka kembali Konsol Play, lalu pilih aplikasi Anda dari daftar Semua Aplikasi.
- Scroll ke bawah, lalu buka Monetisasi > Penyiapan Monetisasi.
- Isi topik lengkap dan simpan perubahan.
Semua peristiwa penagihan Google Play kini akan dipublikasikan di topik tersebut.
Memproses peristiwa penagihan App Store
Selanjutnya, lakukan hal yang sama untuk peristiwa penagihan App Store. Ada dua cara efektif untuk menerapkan penanganan update dalam pembelian untuk App Store. Salah satunya adalah dengan menerapkan webhook yang Anda berikan kepada Apple dan digunakan untuk berkomunikasi dengan server Anda. Cara kedua, yang akan Anda temukan di codelab ini, adalah dengan terhubung ke App Store Server API dan mendapatkan informasi langganan secara manual.
Alasan codelab ini berfokus pada solusi kedua adalah karena Anda harus mengekspos server ke Internet untuk menerapkan webhook.
Dalam lingkungan produksi, idealnya Anda ingin memiliki keduanya. Webhook untuk mendapatkan peristiwa dari App Store, dan Server API jika Anda melewatkan peristiwa atau perlu memeriksa ulang status langganan.
Mulai dengan membuka lib/app_store_purchase_handler.dart
, lalu tambahkan dependensi AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
Ubah konstruktor untuk menambahkan timer yang akan memanggil metode _pullStatus
. Timer ini akan memanggil metode _pullStatus
setiap 10 detik. Anda dapat menyesuaikan durasi timer ini sesuai kebutuhan.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
Kemudian, buat metode _pullStatus sebagai berikut:
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,
));
}
}
}
}
Metode ini berfungsi sebagai berikut:
- Mendapatkan daftar langganan aktif dari Firestore menggunakan IapRepository.
- Untuk setiap pesanan, aplikasi ini meminta status langganan ke App Store Server API.
- Mendapatkan transaksi terakhir untuk pembelian langganan tersebut.
- Memeriksa tanggal habis masa berlaku.
- Memperbarui status langganan di Firestore, jika habis masa berlakunya, langganan akan ditandai sebagai habis masa berlakunya.
Terakhir, tambahkan semua kode yang diperlukan untuk mengonfigurasi akses 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
),
};
Penyiapan App Store
Selanjutnya, siapkan App Store:
- Login ke App Store Connect, lalu pilih Users and Access.
- Buka Jenis Kunci > Pembelian Dalam Aplikasi.
- Ketuk ikon "plus" untuk menambahkan ikon baru.
- Beri nama, misalnya "Kunci codelab".
- Download file p8 yang berisi kunci.
- Salin ke folder aset, dengan nama
SubscriptionKey.p8
. - Salin ID kunci dari kunci yang baru dibuat dan tetapkan ke konstanta
appStoreKeyId
dalam filelib/constants.dart
. - Salin ID Penerbit tepat di bagian atas daftar kunci, dan tetapkan ke konstanta
appStoreIssuerId
dalam filelib/constants.dart
.
Melacak pembelian di perangkat
Cara paling aman untuk melacak pembelian Anda adalah di sisi server karena klien sulit diamankan, tetapi Anda harus memiliki beberapa cara untuk mendapatkan informasi kembali ke klien sehingga aplikasi dapat menindaklanjuti informasi status langganan. Dengan menyimpan pembelian di Firestore, Anda dapat dengan mudah menyinkronkan data ke klien dan terus memperbaruinya secara otomatis.
Anda telah menyertakan IAPRepo di aplikasi, yang merupakan repositori Firestore yang berisi semua data pembelian pengguna di List<PastPurchase> purchases
. Repositori juga berisi hasActiveSubscription,
yang bernilai benar jika ada pembelian dengan productId storeKeySubscription
dengan status yang belum berakhir. Jika pengguna tidak login, daftar akan kosong.
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();
});
}
Semua logika pembelian ada di class DashPurchases
dan merupakan tempat langganan harus diterapkan atau dihapus. Jadi, tambahkan iapRepo
sebagai properti di class dan tetapkan iapRepo
di konstruktor. Selanjutnya, tambahkan pemroses secara langsung di konstruktor, dan hapus pemroses di metode dispose()
. Pada awalnya, pemroses hanya dapat berupa fungsi kosong. Karena IAPRepo
adalah ChangeNotifier
dan Anda memanggil notifyListeners()
setiap kali pembelian di Firestore berubah, metode purchasesUpdate()
selalu dipanggil saat produk yang dibeli berubah.
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
}
Selanjutnya, berikan IAPRepo
ke konstruktor di main.dart.
. Anda bisa mendapatkan repositori menggunakan context.read
karena sudah dibuat di Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
Selanjutnya, tulis kode untuk fungsi purchaseUpdate()
. Di dash_counter.dart,
, metode applyPaidMultiplier
dan removePaidMultiplier
menetapkan pengganda masing-masing ke 10 atau 1, sehingga Anda tidak perlu memeriksa apakah langganan sudah diterapkan. Saat status langganan berubah, Anda juga memperbarui status produk yang dapat dibeli sehingga Anda dapat menampilkan di halaman pembelian bahwa produk tersebut sudah aktif. Tetapkan properti _beautifiedDashUpgrade
berdasarkan apakah upgrade dibeli.
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();
}
}
Sekarang Anda telah memastikan bahwa status langganan dan upgrade selalu terbaru di layanan backend dan disinkronkan dengan aplikasi. Aplikasi akan bertindak sesuai dengan status tersebut dan menerapkan fitur langganan dan upgrade ke game Dash clicker Anda.
12. Selesai!
Selamat. Anda telah menyelesaikan codelab. Anda dapat menemukan kode lengkap untuk codelab ini di folder complete.
Untuk mempelajari lebih lanjut, coba codelab Flutter lain.