Menambahkan pembelian dalam aplikasi ke aplikasi Flutter Anda

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 diberikan, Dash Clicker, berisi game yang menggunakan maskot Dash sebagai mata uang. Anda akan menambahkan opsi pembelian berikut:

  1. Opsi pembelian berulang untuk 2.000 Dash sekaligus.
  2. Pembelian upgrade sekali beli untuk mengubah Dasbor gaya lama menjadi Dasbor gaya modern.
  3. Langganan yang menggandakan klik yang dibuat secara otomatis.

Opsi pembelian pertama memberi pengguna manfaat langsung berupa 2.000 Dash. Item ini tersedia langsung untuk pengguna dan dapat dibeli berkali-kali. Item ini disebut item habis pakai karena langsung digunakan dan dapat digunakan beberapa kali.

Opsi kedua mengupgrade Dasbor menjadi Dasbor yang lebih menarik. Produk ini hanya perlu dibeli sekali dan tersedia selamanya. Pembelian semacam itu disebut tidak habis pakai karena tidak dapat digunakan oleh aplikasi, tetapi berlaku selamanya.

Opsi pembelian ketiga dan terakhir adalah langganan. Selama 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 proses, tetapi di aplikasi produksi, Anda dapat menggunakan jenis layanan backend apa pun.

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

Yang akan Anda build

  • Anda akan memperluas aplikasi untuk mendukung pembelian sekali pakai dan langganan.
  • 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
  • Xcode (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 GitHub CLI, 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 bawah flutter-codelabs/in_app_purchases berisi serangkaian snapshot tentang posisi Anda di akhir setiap langkah yang diberi nama. Kode awal ada di langkah 0, jadi buka seperti berikut:

cd flutter-codelabs/in_app_purchases/step_00

Jika Anda ingin melompati ke depan atau melihat tampilan sesuatu setelah langkah tertentu, lihat di direktori yang dinamai sesuai langkah yang Anda minati. Kode langkah terakhir ada di folder complete.

Menyiapkan project awal

Buka project awal dari step_00/app 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 perlu berkomunikasi dengan App Store dan Play Store untuk mengetahui produk mana yang tersedia dan berapa harganya. Setiap aplikasi diidentifikasi dengan ID unik. Untuk App Store iOS, ini disebut ID paket dan untuk Play Store Android, ini adalah ID aplikasi. Pengidentifikasi 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, lalu tetapkan ID tersebut di setelan project.

Pertama, siapkan ID paket untuk iOS. Untuk melakukannya, buka file Runner.xcworkspace di aplikasi Xcode.

a9fbac80a31e28e0.png

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 menyetel tim Anda.

812f919d965c649a.jpeg

Sekarang Anda dapat menutup Xcode dan kembali ke Android Studio untuk menyelesaikan konfigurasi Android. Untuk melakukannya, buka file build.gradle.kts di android/app, dan ubah applicationId Anda (di baris 24 pada screenshot di bawah) ke ID aplikasi, sama seperti ID paket iOS. Perhatikan bahwa ID untuk toko iOS dan Android tidak harus sama, tetapi menyamakannya akan mengurangi kemungkinan terjadinya error. Oleh karena itu, dalam codelab ini, kita juga akan menggunakan ID yang sama.

e320a49ff2068ac2.png

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 project Anda:

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

Buka pubspec.yaml Anda, dan konfirmasi bahwa Anda kini 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: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

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

4. Menyiapkan App Store

Untuk menyiapkan pembelian dalam aplikasi dan mengujinya di iOS, Anda harus membuat aplikasi baru di App Store dan membuat produk yang dapat dibeli di sana. Anda tidak perlu memublikasikan apa pun atau mengirim aplikasi ke Apple untuk ditinjau. Anda memerlukan akun developer untuk melakukannya. Jika Anda belum memilikinya, daftar ke program developer Apple.

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 Rekening Bank.

11db9fca823e7608.png

Anda akan melihat perjanjian di sini untuk aplikasi gratis dan berbayar. Status aplikasi gratis harus aktif, dan status aplikasi berbayar adalah baru. Pastikan Anda melihat persyaratan, menyetujuinya, dan memasukkan semua informasi yang diperlukan.

74c73197472c9aec.png

Jika semuanya disetel 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.

4a100bbb8cafdbbf.jpeg

Daftarkan ID Aplikasi

Buat ID baru di Apple Developer Portal. Buka developer.apple.com/account/resources/identifiers/list dan klik ikon "plus" di samping header Identifiers.

55d7e592d9a3fc7b.png

Pilih ID Aplikasi

13f125598b72ca77.png

Pilih Aplikasi

41ac4c13404e2526.png

Berikan deskripsi dan tetapkan ID paket agar cocok dengan ID paket ke nilai yang sama seperti yang ditetapkan sebelumnya di XCode.

9d2c940ad80deeef.png

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.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

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 Sandbox untuk membuat akun sandbox baru atau mengelola ID Apple sandbox yang ada.

2ba0f599bcac9b36.png

Sekarang Anda dapat menyiapkan pengguna sandbox di iPhone dengan membuka Setelan > Developer > Akun Apple Sandbox.

74a545210b282ad8.png eaa67752f2350f74.png

Mengonfigurasi pembelian dalam aplikasi

Sekarang Anda akan mengonfigurasi tiga item yang dapat dibeli:

  • dash_consumable_2k: Pembelian item habis pakai yang dapat dibeli berkali-kali, yang memberi pengguna 2.000 Dash (mata uang dalam aplikasi) per pembelian.
  • dash_upgrade_3d: Pembelian "upgrade" tidak sekali pakai 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.

a118161fac83815a.png

Buka Pembelian Dalam Aplikasi.

Buat pembelian dalam aplikasi Anda dengan ID yang ditentukan:

  1. Siapkan dash_consumable_2k sebagai Dapat Dikonsumsi. Gunakan dash_consumable_2k sebagai ID Produk. Nama referensi hanya digunakan di App Store Connect, cukup tetapkan ke dash consumable 2k. 1f8527fc03902099.png Siapkan ketersediaan. Produk harus tersedia di negara pengguna sandbox. bd6b2ce2d9314e6e.png Tambahkan harga dan tetapkan harga ke $1.99 atau yang setara dalam mata uang lain. 926b03544ae044c4.png Tambahkan pelokalan untuk pembelian Anda. Panggil pembelian Spring is in the air dengan 2000 dashes fly out sebagai deskripsi. e26dd4f966dcfece.png Tambahkan screenshot ulasan. Konten tidak penting kecuali jika produk dikirim untuk ditinjau, tetapi konten diperlukan agar produk berada dalam status "Siap Dikirim", yang diperlukan saat aplikasi mengambil produk dari App Store. 25171bfd6f3a033a.png
  2. Siapkan dash_upgrade_3d sebagai Tidak dapat digunakan. Gunakan dash_upgrade_3d sebagai ID Produk. Tetapkan nama referensi ke dash upgrade 3d. Panggil pembelian 3D Dash dengan Brings your dash back to the future sebagai deskripsi. Tetapkan harga ke $0.99. Konfigurasi ketersediaan dan upload screenshot ulasan dengan cara yang sama seperti untuk produk dash_consumable_2k. 83878759f32a7d4a.png
  3. Siapkan dash_subscription_doubler sebagai Langganan perpanjangan otomatis. Alur untuk langganan sedikit berbeda. Pertama, Anda harus membuat grup langganan. Jika beberapa langganan merupakan bagian dari grup yang sama, pengguna hanya dapat berlangganan salah satunya pada waktu yang sama, tetapi dapat melakukan upgrade atau downgrade di antara langganan ini. Cukup panggil grup ini subscriptions. 393a44b09f3cd8bf.png Lalu tambahkan pelokalan untuk grup langganan. 595aa910776349bd.png Selanjutnya, Anda akan membuat langganan. Tetapkan Nama Referensi ke dash subscription doubler dan ID Produk ke dash_subscription_doubler. 7bfff7bbe11c8eec.png Selanjutnya, pilih durasi langganan 1 minggu dan pelokalan. Beri nama langganan ini Jet Engine dengan deskripsi Doubles your clicks. Tetapkan harga ke $0.49. Konfigurasi ketersediaan dan upload screenshot ulasan dengan cara yang sama seperti untuk produk dash_consumable_2k. 44d18e02b926a334.png

Sekarang Anda akan melihat produk dalam daftar:

17f242b5c1426b79.png d71da951f595054a.png

5. Menyiapkan Play Store

Seperti halnya App Store, Anda juga memerlukan akun developer untuk Play Store. Jika Anda belum memilikinya, daftar akun.

Membuat aplikasi baru

Buat aplikasi baru di Konsol Google Play:

  1. Buka Konsol Play.
  2. Pilih Semua aplikasi > Buat aplikasi.
  3. 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.
  4. Tentukan bahwa aplikasi Anda adalah game. Anda dapat mengubahnya nanti.
  5. Tentukan apakah aplikasi Anda gratis atau berbayar.
  6. Lengkapi pernyataan Panduan konten dan Hukum ekspor Amerika Serikat.
  7. Pilih Buat aplikasi.

Setelah aplikasi Anda dibuat, buka dasbor, lalu selesaikan semua tugas di bagian Siapkan aplikasi Anda. Di sini, Anda memberikan beberapa informasi tentang aplikasi Anda, seperti rating konten dan screenshot. 13845badcf9bc1db.png

Menandatangani aplikasi

Untuk dapat menguji pembelian dalam aplikasi, Anda memerlukan setidaknya satu build yang diupload ke Google Play.

Untuk melakukannya, Anda memerlukan build rilis yang ditandatangani dengan sesuatu selain kunci debug.

Membuat keystore

Jika Anda sudah memiliki keystore, lanjutkan ke langkah berikutnya. Jika tidak, buat 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. Pertahankan

keystore

file bersifat pribadi; jangan check in ke kontrol sumber publik.

Merujuk 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

Konfigurasi penandatanganan untuk aplikasi Anda dengan mengedit file <your app dir>/android/app/build.gradle.kts.

Tambahkan informasi keystore dari file properti Anda sebelum blok android:

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

plugins {
    // omitted
}

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

android {
    // omitted
}

Muat file key.properties ke objek keystoreProperties.

Perbarui blok buildTypes menjadi:

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

Konfigurasi blok signingConfigs dalam file build.gradle.kts modul Anda dengan informasi konfigurasi penandatanganan:

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

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

Build rilis aplikasi Anda kini akan ditandatangani secara otomatis.

Untuk mengetahui informasi selengkapnya tentang menandatangani aplikasi, lihat Menandatangani aplikasi Anda di developer.android.com.

Mengupload build pertama Anda

Setelah aplikasi dikonfigurasi untuk penandatanganan, Anda akan dapat membangun aplikasi dengan menjalankan:

flutter build appbundle

Perintah ini menghasilkan build rilis secara default dan outputnya dapat ditemukan di <your app dir>/build/app/outputs/bundle/release/

Dari dasbor di Konsol Google Play, buka Uji dan rilis > Pengujian > Pengujian tertutup, lalu buat rilis pengujian tertutup baru.

Selanjutnya, upload paket aplikasi app-release.aab yang dihasilkan oleh perintah build.

Klik Simpan, lalu klik Tinjau rilis.

Terakhir, klik Mulai peluncuran ke Pengujian tertutup untuk mengaktifkan rilis pengujian tertutup.

Menyiapkan pengguna pengujian

Agar dapat menguji pembelian dalam aplikasi, Akun Google penguji Anda harus ditambahkan di Konsol Google Play di dua lokasi:

  1. Ke jalur pengujian tertentu (Pengujian internal)
  2. Sebagai penguji lisensi

Pertama, mulai dengan menambahkan penguji ke jalur pengujian internal. Kembali ke Uji dan rilis > Pengujian > Pengujian internal, lalu klik tab Penguji.

a0d0394e85128f84.png

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:

  1. Kembali ke tampilan Semua aplikasi di Konsol Google Play.
  2. Buka Setelan > Pengujian lisensi.
  3. Tambahkan alamat email yang sama dari penguji yang harus dapat menguji pembelian dalam aplikasi.
  4. Tetapkan License response ke RESPOND_NORMALLY.
  5. Klik Simpan perubahan.

a1a0f9d3e55ea8da.png

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 item 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 sekali pakai dan item tidak sekali pakai.

  1. Buka Konsol Google Play, lalu pilih aplikasi Anda.
  2. Buka Monetisasi > Produk > Produk dalam aplikasi.
  3. Klik Buat produkc8d66e32f57dee21.png
  4. Masukkan semua informasi yang diperlukan untuk produk Anda. Pastikan ID produk sama persis dengan ID yang ingin Anda gunakan.
  5. Klik Simpan.
  6. Klik Aktifkan.
  7. Ulangi proses untuk pembelian "upgrade" produk tidak habis pakai.

Selanjutnya, tambahkan langganan:

  1. Buka Konsol Google Play, lalu pilih aplikasi Anda.
  2. Buka Monetisasi > Produk > Langganan.
  3. Klik Buat langganan32a6a9eefdb71dd0.png
  4. Masukkan semua informasi yang diperlukan untuk langganan Anda. Pastikan ID produk sama persis dengan ID yang ingin Anda gunakan.
  5. 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 bereaksi terhadap peristiwa penagihan dari app store.
  • Anda dapat melacak pembelian dalam database.
  • Pengguna tidak akan dapat menipu aplikasi Anda untuk menyediakan fitur premium dengan memundurkan jam sistem mereka.

Meskipun ada banyak cara untuk menyiapkan layanan backend, Anda akan melakukannya menggunakan Cloud Functions dan Firestore, dengan menggunakan Firebase milik Google sendiri.

Penulisan backend dianggap di luar cakupan codelab ini, sehingga kode awal sudah menyertakan project Firebase yang menangani pembelian dasar untuk membantu Anda memulai.

Plugin Firebase juga disertakan dengan aplikasi starter.

Yang perlu Anda lakukan adalah membuat project Firebase sendiri, mengonfigurasi aplikasi dan backend untuk Firebase, lalu men-deploy backend.

Membuat project Firebase

Buka Firebase console, lalu buat project Firebase baru. Untuk contoh ini, sebut projectnya Dash Clicker.

Di aplikasi backend, Anda mengikat pembelian ke pengguna tertentu, sehingga Anda memerlukan autentikasi. Untuk melakukannya, gunakan modul autentikasi Firebase dengan login dengan Google.

  1. Dari dasbor Firebase, buka Authentication dan aktifkan, jika diperlukan.
  2. Buka tab Sign-in method, lalu aktifkan penyedia login Google.

fe2e0933d6810888.png

Karena Anda juga akan menggunakan database Firestore Firebase, aktifkan juga opsi ini.

d02d641821c71e2c.png

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 dengan menggunakan FlutterFire CLI. Ikuti petunjuk seperti yang dijelaskan di halaman penyiapan.

Saat menjalankan flutterfire configure, pilih project yang baru saja Anda buat pada 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.

? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
  macos
  web

Saat diminta untuk mengganti firebase_options.dart, pilih ya.

? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes

Menyiapkan Firebase untuk Android: Langkah selanjutnya

Dari dasbor Firebase, buka Project Overview, pilih Settings, lalu pilih tab General.

Scroll ke bawah ke Your apps, lalu pilih aplikasi dashclicker (android).

b22d46a759c0c834.png

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 Anda

Di root project aplikasi Flutter Anda, 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 .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 dalam dialog modal pengiriman aplikasi.

Terakhir, jalankan perintah flutterfire configure lagi untuk memperbarui aplikasi agar menyertakan konfigurasi penandatanganan.

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

Menyiapkan Firebase untuk iOS: Langkah selanjutnya

Buka ios/Runner.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 dokumentasi paket google_sign_in untuk mengetahui informasi selengkapnya.) Dalam hal ini, file-nya adalah ios/Runner/Info.plist.

Pasangan nilai kunci sudah ditambahkan, tetapi nilainya harus diganti:

  1. Dapatkan nilai untuk REVERSED_CLIENT_ID dari file GoogleService-Info.plist, tanpa elemen <string>..</string> yang mengelilinginya.
  2. Ganti nilai dalam file ios/Runner/Info.plist Anda di bagian kunci 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>

Sekarang Anda telah menyelesaikan penyiapan Firebase.

7. Memproses info terbaru pembelian

Di bagian codelab ini, Anda akan menyiapkan aplikasi untuk membeli produk. Proses ini mencakup mendengarkan update dan error pembelian setelah aplikasi dimulai.

Mendengarkan info terbaru 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 Dasbor 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 update 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 cara untuk meniru koneksi. Untuk melakukannya, buat metode instance yang dapat diganti dalam pengujian, dan 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!;
  }
}

Perbarui pengujian sebagai berikut:

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 DashPurchasesChangeNotifier. Pada tahap ini, hanya ada DashCounter yang dapat Anda tambahkan ke Dasbor yang Anda beli.

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 dalam konstruktor. Project ini disiapkan agar secara default tidak dapat bernilai null (NNBD), yang berarti bahwa properti yang tidak dideklarasikan dapat bernilai null harus memiliki nilai yang tidak bernilai null. Penentu late memungkinkan Anda menunda penentuan nilai ini.

Di konstruktor, dapatkan aliran purchaseUpdated dan mulai memproses aliran. Dalam metode dispose(), batalkan langganan streaming.

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.notAvailable;         // Modify this line
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter) {                            // Add from here
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }                                                        // To here.

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
                                                           // Add from here
  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    //Handle error here
  }                                                        // To here.
}

Sekarang, aplikasi menerima update pembelian, jadi 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 dengan produk asli yang dapat dibeli. Produk ini dimuat dari toko, ditampilkan dalam daftar, dan dibeli saat mengetuk produk.

Adapt 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;
    }
  }

Saat toko tersedia, muat pembelian yang tersedia. Dengan penyiapan Google Play dan App Store sebelumnya, Anda akan melihat storeKeyConsumable, storeKeySubscription,, dan storeKeyUpgrade. Jika pembelian yang diharapkan tidak tersedia, cetak informasi ini ke konsol; Anda mungkin juga ingin mengirimkan 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();                                       // Add this line
  }

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 pengguna yang digunakan pada 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 toko Android dan iOS jika dikonfigurasi dengan benar. Perhatikan bahwa mungkin perlu beberapa saat sebelum pembelian tersedia saat dimasukkan ke konsol masing-masing.

ca1a9f97c21e552d.png

Kembali ke dash_purchases.dart, lalu terapkan fungsi untuk membeli produk. Anda hanya perlu memisahkan item sekali pakai dari item tidak sekali pakai. Produk upgrade dan langganan adalah produk sekali beli.

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 getter 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. Penting untuk memanggil completePurchase setelah menangani pembelian agar Play Store 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, kerjakan dari folder dart-backend/ sebagai root.

Pastikan Anda telah menginstal alat berikut:

Ringkasan project dasar

Karena beberapa bagian project ini dianggap di luar cakupan codelab ini, bagian tersebut disertakan dalam kode awal. Sebaiknya tinjau terlebih dahulu kode awal yang sudah ada sebelum Anda memulai, untuk mendapatkan gambaran tentang cara Anda akan menyusunnya.

Kode backend ini dapat berjalan secara lokal di komputer Anda, Anda tidak perlu men-deploy-nya untuk menggunakannya. Namun, Anda harus dapat terhubung dari perangkat pengembangan (Android atau iPhone) ke mesin tempat server akan berjalan. Untuk itu, mereka harus berada di jaringan yang sama, dan Anda perlu mengetahui alamat IP komputer Anda.

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 menyajikan endpoint API. Secara default, server tidak menyediakan rute apa pun. Nanti, Anda akan membuat rute untuk menangani proses verifikasi pembelian.

Salah 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 mengupdate pembelian di Firestore, serta semua class untuk pembelian tersebut.

Menyiapkan akses Firebase

Untuk mengakses Firebase Firestore, Anda memerlukan kunci akses akun layanan. Buat satu dengan membuka setelan project Firebase dan membuka bagian Service accounts, lalu pilih Generate new private key.

27590fc77ae94ad4.png

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.

  1. Buka halaman Google Play Android Developer API di Konsol Google Cloud. 629f0bd8e6b50be8.png Jika Konsol Google Play meminta Anda membuat atau menautkan ke project yang sudah ada, lakukan terlebih dahulu, lalu kembali ke halaman ini.
  2. Selanjutnya, buka halaman Akun layanan, lalu klik + Buat akun layanan. 8dc97e3b1262328a.png
  3. Masukkan Service account name, lalu klik Create and continue. 4fe8106af85ce75f.png
  4. Pilih peran Pub/Sub Subscriber, lalu klik Done. a5b6fa6ea8ee22d.png
  5. Setelah akun dibuat, buka Kelola kunci. eb36da2c1ad6dd06.png
  6. Pilih Tambahkan kunci > Buat kunci baru. e92db9557a28a479.png
  7. Buat dan download kunci JSON. 711d04f2f4176333.png
  8. Ganti nama file yang didownload menjadi service-account-google-play.json,, lalu pindahkan ke direktori assets/.
  9. Selanjutnya, buka halaman Pengguna dan izin di Konsol Play28fffbfc35b45f97.png
  10. Klik Undang pengguna baru dan masukkan alamat email akun layanan yang dibuat sebelumnya. Anda dapat menemukan email di tabel pada halaman Akun layanane3310cc077f397d.png
  11. Berikan izin Melihat data keuangan dan Mengelola pesanan dan langganan untuk aplikasi. a3b8cf2b660d1900.png
  12. Klik Undang pengguna.

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 Anda.

Menyiapkan akses Apple App Store

Untuk mengakses App Store guna memverifikasi pembelian, Anda harus menyiapkan secret bersama:

  1. Buka App Store Connect.
  2. Buka Aplikasi Saya, lalu pilih aplikasi Anda.
  3. Di navigasi sidebar, buka Umum > Informasi aplikasi.
  4. Klik Kelola di bagian header App-Specific Shared Secret. ad419782c5fbacb2.png
  5. Buat rahasia baru, lalu salin. b5b72a357459b0e5.png
  6. Buka lib/constants.dart, dan ganti nilai appStoreSharedSecret dengan rahasia 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, seperti com.example.dashclicker
  • appStoreSharedSecret: Secret bersama untuk mengakses App Store Connect guna melakukan verifikasi pembelian.
  • bundleId: ID Paket yang digunakan di iOS, seperti com.example.dashclicker

Anda dapat mengabaikan konstanta lainnya untuk saat ini.

10. Memverifikasi pembelian

Alur umum untuk memverifikasi pembelian serupa untuk iOS dan Android.

Untuk kedua toko, aplikasi Anda 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 membuat 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 mengirimkan pembelian ke layanan backend, Anda harus 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 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

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

  @override
  Widget build(BuildContext context) {                     // Update from here
    var firebaseNotifier = context.watch<FirebaseNotifier>();
    if (firebaseNotifier.state == FirebaseState.loading) {
      return _PurchasesLoading();
    } else if (firebaseNotifier.state == FirebaseState.notAvailable) {
      return _PurchasesNotAvailable();
    }

    if (!firebaseNotifier.loggedIn) {
      return const LoginPage();
    }                                                      // To here.

    // ...

Panggil endpoint verifikasi dari aplikasi

Di aplikasi, buat fungsi _verifyPurchase(PurchaseDetails purchaseDetails) yang memanggil endpoint /verifypurchase di backend Dart Anda menggunakan panggilan http post.

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, konfigurasi IP server ke alamat IP komputer lokal Anda.

lib/logic/dash_purchases.dart

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

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

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

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

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

  final iapConnection = IAPConnection.instance;

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

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 getter untuk Pengguna di FirebaseNotifier, sehingga Anda dapat meneruskan ID pengguna ke fungsi verifikasi pembelian.

lib/logic/firebase_notifier.dart

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

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

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

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 menentukan lebih lanjut hal ini, misalnya, untuk menerapkan langganan uji coba saat Play Store tidak tersedia untuk sementara. Namun, untuk contoh ini, terapkan pembelian saat 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(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

Di aplikasi, semuanya kini siap untuk memvalidasi pembelian.

Menyiapkan layanan backend

Selanjutnya, siapkan backend untuk memverifikasi pembelian di backend.

Membangun handler pembelian

Karena alur verifikasi untuk kedua toko hampir identik, siapkan class PurchaseHandler abstrak dengan penerapan terpisah untuk setiap toko.

be50c207c5a2a519.png

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 toko.

Selain itu, untuk mempermudah penggunaan handler pembelian ini, 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, tetapi tetap memiliki implementasi terpisah.

Class ProductData berisi informasi dasar tentang berbagai produk yang dapat dibeli, yang mencakup ID produk (terkadang juga disebut 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 penerapan 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 handler; Anda akan membahasnya nanti.

Seperti yang mungkin Anda perhatikan, konstruktor mengambil instance IapRepository. Penangan 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, lalu 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,
  }) async {
    return true;
  }

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

Bagus! Sekarang Anda memiliki dua handler pembelian. Selanjutnya, buat endpoint API verifikasi pembelian.

Menggunakan pengendali pembelian

Buka bin/server.dart dan buat endpoint API menggunakan shelf_route:

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

Future<void> main() async {
  final router = Router();

  final purchaseHandlers = await _createPurchaseHandlers();

  router.post('/verifypurchase', (Request request) async {
    final dynamic payload = json.decode(await request.readAsString());

    final (:userId, :source, :productData, :token) = getPurchaseData(payload);

    final result = await purchaseHandlers[source]!.verifyPurchase(
      userId: userId,
      productData: productData,
      token: token,
    );

    if (result) {
      return Response.ok('all good!');
    } else {
      return Response.internalServerError();
    }
  });

  await serveHandler(router.call);
}

({String userId, String source, ProductData productData, String token})
getPurchaseData(dynamic payload) {
  if (payload case {
    'userId': String userId,
    'source': String source,
    'productId': String productId,
    'verificationData': String token,
  }) {
    return (
      userId: userId,
      source: source,
      productData: productDataMap[productId]!,
      token: token,
    );
  } else {
    throw const FormatException('Unexpected JSON');
  }
}

Kode melakukan hal berikut:

  1. Tentukan endpoint POST yang akan dipanggil dari aplikasi yang Anda buat sebelumnya.
  2. Dekode payload JSON dan ekstrak informasi berikut:
    1. userId: ID pengguna yang login
    2. source: Toko yang digunakan, app_store atau google_play.
    3. productData: Diperoleh dari productDataMap yang Anda buat sebelumnya.
    4. token: Berisi data verifikasi yang akan dikirim ke toko.
  3. Panggilan ke metode verifyPurchase, baik untuk GooglePlayPurchaseHandler maupun AppStorePurchaseHandler, bergantung pada sumbernya.
  4. Jika verifikasi berhasil, metode akan menampilkan Response.ok ke klien.
  5. Jika verifikasi gagal, metode akan menampilkan Response.internalServerError ke klien.

Setelah membuat endpoint API, Anda perlu mengonfigurasi dua handler pembelian. Hal ini mengharuskan Anda memuat kunci akun layanan yang Anda peroleh di langkah sebelumnya dan mengonfigurasi akses ke berbagai layanan, termasuk Android Publisher API dan Firebase Firestore API. Kemudian, buat dua handler pembelian dengan dependensi yang berbeda:

bin/server.dart

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

import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
  // Configure Android Publisher API access
  final serviceAccountGooglePlay =
      File('assets/service-account-google-play.json').readAsStringSync();
  final clientCredentialsGooglePlay =
      auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
  final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
  ]);
  final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);

  // Configure Firestore API access
  final serviceAccountFirebase =
      File('assets/service-account-firebase.json').readAsStringSync();
  final clientCredentialsFirebase =
      auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
  final clientFirebase =
      await auth.clientViaServiceAccount(clientCredentialsFirebase, [
    fs.FirestoreApi.cloudPlatformScope,
  ]);
  final firestoreApi = fs.FirestoreApi(clientFirebase);
  final dynamic json = jsonDecode(serviceAccountFirebase);
  final projectId = json['project_id'] as String;
  final iapRepository = IapRepository(firestoreApi, projectId);

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };
}

Verifikasi pembelian Android: Terapkan handler pembelian

Selanjutnya, lanjutkan penerapan handler pembelian Google Play.

Google telah menyediakan paket Dart untuk berinteraksi dengan API yang Anda butuhkan untuk memverifikasi pembelian. Anda telah menginisialisasinya dalam file server.dart dan sekarang menggunakannya di class GooglePlayPurchaseHandler.

Terapkan handler untuk pembelian jenis non-langganan:

lib/google_play_purchase_handler.dart

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.products.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Purchases response: ${response.toJson()}');

      // Make sure an order ID exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = response.orderId!;

      final purchaseData = NonSubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.purchaseTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _nonSubscriptionStatusFrom(response.purchaseState),
        userId: userId,
        iapSource: IAPSource.googleplay,
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we don't know the user ID, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle NonSubscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle NonSubscription: $e\n');
    }
    return false;
  }

Anda dapat memperbarui handler 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 don't know the user ID, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

Tambahkan metode berikut untuk memfasilitasi parsing ID pesanan, serta dua metode untuk mem-parsing 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 di database.

Selanjutnya, lanjutkan ke pembelian App Store untuk iOS.

Verifikasi pembelian iOS: Terapkan handler 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 mempermudah 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 handler. 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 {

    // See next step
  }

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) {
      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;
    }
  }

Pembelian App Store Anda kini harus diverifikasi dan disimpan dalam 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. Memantau pembelian

Cara yang direkomendasikan untuk melacak pembelian pengguna Anda adalah di layanan backend. Hal ini karena backend Anda dapat merespons peristiwa dari toko dan dengan demikian lebih kecil kemungkinannya mengalami informasi yang sudah tidak berlaku karena caching, serta lebih kecil kemungkinannya dimanipulasi.

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 perpanjangan langganan. Anda dapat memproses peristiwa ini di backend untuk menjaga agar pembelian di database Anda tetap 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. Pada dasarnya, ini adalah antrean pesan yang dapat digunakan untuk memublikasikan dan menggunakan pesan.

Karena ini adalah fungsi khusus untuk Google Play, Anda menyertakan fungsi ini di 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 _pullMessageFromPubSub setiap sepuluh detik. Anda dapat menyesuaikan Durasi sesuai preferensi Anda.

Kemudian, buat _pullMessageFromPubSub

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(maxMessages: 1000);
    final topicName =
        'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

  Future<void> _processMessage(String data64, String? ackId) async {
    final dataRaw = utf8.decode(base64Decode(data64));
    print('Received data: $dataRaw');
    final dynamic data = jsonDecode(dataRaw);
    if (data['testNotification'] != null) {
      print('Skip test messages');
      if (ackId != null) {
        await _ackMessage(ackId);
      }
      return;
    }
    final dynamic subscriptionNotification = data['subscriptionNotification'];
    final dynamic oneTimeProductNotification =
        data['oneTimeProductNotification'];
    if (subscriptionNotification != null) {
      print('Processing Subscription');
      final subscriptionId =
          subscriptionNotification['subscriptionId'] as String;
      final purchaseToken = subscriptionNotification['purchaseToken'] as String;
      final productData = productDataMap[subscriptionId]!;
      final result = await handleSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else if (oneTimeProductNotification != null) {
      print('Processing NonSubscription');
      final sku = oneTimeProductNotification['sku'] as String;
      final purchaseToken =
          oneTimeProductNotification['purchaseToken'] as String;
      final productData = productDataMap[sku]!;
      final result = await handleNonSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else {
      print('invalid data');
    }
  }

  /// ACK Messages from Pub/Sub
  Future<void> _ackMessage(String id) async {
    print('ACK Message');
    final request = pubsub.AcknowledgeRequest(ackIds: [id]);
    final subscriptionName =
        'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

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 masuk dan mendapatkan informasi terbaru tentang setiap pembelian, baik langganan maupun non-langganan, dengan memanggil handleSubscription atau handleNonSubscription yang ada jika perlu.

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

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

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

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,                                           // Add this line
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };

Penyiapan Google Play

Anda telah menulis kode untuk menggunakan peristiwa penagihan dari topik pub/sub, tetapi Anda belum membuat topik pub/sub, dan Anda juga tidak memublikasikan peristiwa penagihan apa pun. Sekarang saatnya menyiapkan fitur ini.

Pertama, buat topik pub/sub:

  1. Tetapkan nilai googleCloudProjectId di constants.dart ke ID Project Google Cloud Anda.
  2. Buka halaman Cloud Pub/Sub di Konsol Google Cloud.
  3. Pastikan Anda berada di project Firebase, lalu klik + Create Topic. d5ebf6897a0a8bf5.png
  4. Beri nama topik baru, yang sama dengan nilai yang ditetapkan untuk googlePlayPubsubBillingTopic di constants.dart. Dalam hal ini, beri nama play_billing. Jika Anda memilih sesuatu yang lain, pastikan untuk memperbarui constants.dart. Buat topik. 20d690fc543c4212.png
  5. Di daftar topik pub/sub Anda, klik tiga titik vertikal untuk topik yang baru saja Anda buat, lalu klik Lihat izin. ea03308190609fb.png
  6. Di sidebar di sebelah kanan, pilih Tambahkan prinsipal.
  7. Di sini, tambahkan google-play-developer-notifications@system.gserviceaccount.com, lalu berikan peran sebagai Pub/Sub Publisher. 55631ec0549215bc.png
  8. Simpan perubahan izin.
  9. Salin Nama topik dari topik yang baru saja Anda buat.
  10. Buka kembali Konsol Play, lalu pilih aplikasi Anda dari daftar Semua Aplikasi.
  11. Scroll ke bawah, lalu buka Monetisasi > Penyiapan Monetisasi.
  12. Isi topik lengkap dan simpan perubahan Anda. 7e5e875dc6ce5d54.png

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 mereka gunakan untuk berkomunikasi dengan server Anda. Cara kedua, yang akan Anda temukan dalam codelab ini, adalah dengan terhubung ke App Store Server API dan mendapatkan informasi langganan secara manual.

Alasan mengapa 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, dan menambahkan dependensi AppStoreServerAPI:

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

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

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

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where(
      (element) =>
          element.type == ProductType.subscription &&
          element.iapSource == IAPSource.appstore,
    );
    for (final purchase in appStoreSubscriptions) {
      final status = await appStoreServerAPI.getAllSubscriptionStatuses(
        purchase.orderId,
      );
      // Obtain all subscriptions for the order ID.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
            transaction.transactionInfo.expiresDate ?? 0,
          );
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(
            SubscriptionPurchase(
              userId: null,
              productId: transaction.transactionInfo.productId,
              iapSource: IAPSource.appstore,
              orderId: transaction.originalTransactionId,
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate,
              ),
              type: ProductType.subscription,
              expiryDate: expirationDate,
              status: isExpired
                  ? SubscriptionStatus.expired
                  : SubscriptionStatus.active,
            ),
          );
        }
      }
    }
  }

Metode ini berfungsi sebagai berikut:

  1. Mendapatkan daftar langganan aktif dari Firestore menggunakan IapRepository.
  2. Untuk setiap pesanan, aplikasi akan meminta status langganan ke App Store Server API.
  3. Mendapatkan transaksi terakhir untuk pembelian langganan tersebut.
  4. Memeriksa tanggal habis masa berlaku.
  5. Memperbarui status langganan di Firestore, jika masa berlakunya telah berakhir, langganan akan ditandai demikian.

Terakhir, tambahkan semua kode yang diperlukan untuk mengonfigurasi akses App Store Server API:

bin/server.dart

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


  // add from here
  final subscriptionKeyAppStore = File(
    'assets/SubscriptionKey.p8',
  ).readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here

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

Penyiapan App Store

Selanjutnya, siapkan App Store:

  1. Login ke App Store Connect, lalu pilih Users and Access.
  2. Buka Integrasi > Kunci > Pembelian Dalam Aplikasi.
  3. Ketuk ikon "plus" untuk menambahkan yang baru.
  4. Beri nama, seperti "Kunci codelab".
  5. Download file p8 yang berisi kunci.
  6. Salin ke folder aset, dengan nama SubscriptionKey.p8.
  7. Salin ID kunci dari kunci yang baru dibuat dan tetapkan ke konstanta appStoreKeyId dalam file lib/constants.dart.
  8. Salin Issuer ID tepat di bagian atas daftar kunci, lalu tetapkan ke konstanta appStoreIssuerId dalam file lib/constants.dart.

9540ea9ada3da151.png

Melacak pembelian di perangkat

Cara paling aman untuk melacak pembelian Anda adalah di sisi server karena klien sulit diamankan, tetapi Anda harus memiliki cara untuk mendapatkan informasi kembali ke klien sehingga aplikasi dapat bertindak berdasarkan informasi status langganan. Dengan menyimpan pembelian di Firestore, Anda dapat 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 ini juga berisi hasActiveSubscription, yang bernilai benar jika ada pembelian dengan productId storeKeySubscription dengan status yang tidak habis masa berlakunya. 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((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

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

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

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

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

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

  @override
  void dispose() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);                 // Add this line
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

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>(),                         // Add this line
          ),
          lazy: false,
        ),

Selanjutnya, tulis kode untuk fungsi purchaseUpdate(). Di dash_counter.dart,, metode applyPaidMultiplier dan removePaidMultiplier menetapkan pengganda 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 menunjukkan 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 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 android_studio_folder.png complete.

Untuk mempelajari lebih lanjut, coba codelab Flutter lainnya.