افزودن خریدهای درون برنامه ای به برنامه Flutter خود

۱. مقدمه

افزودن خریدهای درون‌برنامه‌ای به یک برنامه Flutter نیاز به تنظیم صحیح فروشگاه‌های App و Play، تأیید خرید و اعطای مجوزهای لازم، مانند مزایای اشتراک، دارد.

در این آزمایشگاه کد، شما سه نوع خرید درون‌برنامه‌ای را به یک برنامه (که برای شما ارائه شده است) اضافه خواهید کرد و این خریدها را با استفاده از یک backend دارت با Firebase تأیید خواهید کرد. برنامه ارائه شده، Dash Clicker، شامل یک بازی است که از نماد Dash به عنوان واحد پول استفاده می‌کند. شما گزینه‌های خرید زیر را اضافه خواهید کرد:

  1. یک گزینه خرید تکرارپذیر برای ۲۰۰۰ دش به طور همزمان.
  2. یک خرید ارتقاء یک‌باره برای تبدیل Dash به سبک قدیمی به Dash مدرن.
  3. اشتراکی که کلیک‌های خودکار ایجاد شده را دو برابر می‌کند.

اولین گزینه خرید، به کاربر ۲۰۰۰ دش (Dash) به طور مستقیم می‌دهد. این دش‌ها مستقیماً در دسترس کاربر هستند و می‌توانند بارها خریداری شوند. این دش‌ها، مصرفی نامیده می‌شوند زیرا مستقیماً مصرف می‌شوند و می‌توانند چندین بار مصرف شوند.

گزینه دوم، دش (Dash) را به یک دش (Dash) زیباتر ارتقا می‌دهد. این خرید فقط یک بار قابل انجام است و برای همیشه در دسترس خواهد بود. چنین خریدی غیرقابل مصرف نامیده می‌شود زیرا توسط برنامه قابل مصرف نیست اما برای همیشه معتبر است.

سومین و آخرین گزینه خرید، اشتراک است. تا زمانی که اشتراک فعال باشد، کاربر سریع‌تر دش دریافت می‌کند، اما وقتی پرداخت هزینه اشتراک را متوقف کند، مزایا نیز از بین می‌روند.

سرویس backend (که برای شما نیز ارائه شده است) به عنوان یک برنامه Dart اجرا می‌شود، تأیید می‌کند که خریدها انجام شده‌اند و آنها را با استفاده از Firestore ذخیره می‌کند. Firestore برای آسان‌تر کردن فرآیند استفاده می‌شود، اما در برنامه اصلی خود می‌توانید از هر نوع سرویس backend استفاده کنید.

۳۰۰۱۲۳۴۱۶ebc8dc1.png7145d0fffe6ea741.png646317a79be08214.png

آنچه خواهید ساخت

  • شما یک برنامه را برای پشتیبانی از خریدهای مصرفی و اشتراک‌ها گسترش خواهید داد.
  • همچنین یک برنامه‌ی بک‌اند دارت را برای تأیید و ذخیره‌ی اقلام خریداری‌شده توسعه خواهید داد.

آنچه یاد خواهید گرفت

  • نحوه پیکربندی اپ استور و پلی استور با محصولات قابل خرید.
  • نحوه ارتباط با فروشگاه‌ها برای تأیید خریدها و ذخیره آنها در Firestore.
  • نحوه مدیریت خریدها در برنامه شما.

آنچه نیاز دارید

۲. محیط توسعه را راه‌اندازی کنید

برای شروع این آزمایشگاه کد، کد را دانلود کنید و شناسه بسته (bundle identifier) ​​را برای iOS و نام بسته (package name) را برای اندروید تغییر دهید.

کد را دانلود کنید

برای کپی کردن مخزن GitHub از طریق خط فرمان، از دستور زیر استفاده کنید:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

یا اگر ابزار cli گیت‌هاب را نصب کرده‌اید، از دستور زیر استفاده کنید:

gh repo clone flutter/codelabs flutter-codelabs

کد نمونه در دایرکتوری flutter-codelabs که شامل کد مجموعه‌ای از codelabs است، کپی می‌شود. کد مربوط به این codelab در flutter-codelabs/in_app_purchases قرار دارد.

ساختار دایرکتوری زیر flutter-codelabs/in_app_purchases شامل مجموعه‌ای از تصاویر لحظه‌ای از جایی است که باید در انتهای هر مرحله‌ی نامگذاری شده باشید. کد آغازین در مرحله‌ی ۰ قرار دارد، بنابراین به صورت زیر به آن بروید:

cd flutter-codelabs/in_app_purchases/step_00

اگر می‌خواهید به مرحله بعدی بروید یا ببینید که بعد از یک مرحله چه شکلی باید داشته باشد، به دایرکتوری که نام آن مرحله مورد نظر شماست نگاه کنید. کد آخرین مرحله در زیر پوشه complete قرار دارد.

پروژه اولیه را تنظیم کنید

پروژه اولیه را از step_00/app در IDE مورد علاقه خود باز کنید. ما از اندروید استودیو برای اسکرین‌شات‌ها استفاده کردیم، اما ویژوال استودیو کد نیز گزینه بسیار خوبی است. با هر دو ویرایشگر، مطمئن شوید که آخرین افزونه‌های Dart و Flutter نصب شده‌اند.

The apps you are going to make need to communicate with the App Store and Play Store to know which products are available and for what price. Every app is identified by a unique ID. For the iOS App Store this is called the bundle identifier and for the Android Play Store this is the application ID. These identifiers are usually made using a reverse domain name notation. For example when making an in app purchase app for flutter.dev you would use dev.flutter.inapppurchase . Think of an identifier for your app, you are now going to set that in the project settings.

ابتدا، شناسه بسته نرم‌افزاری را برای iOS تنظیم کنید. برای انجام این کار، فایل Runner.xcworkspace را در برنامه Xcode باز کنید.

a9fbac80a31e28e0.png

در ساختار پوشه Xcode، پروژه Runner در بالا قرار دارد و اهداف Flutter ، Runner و Products در زیر پروژه Runner قرار دارند. برای ویرایش تنظیمات پروژه خود، روی Runner دوبار کلیک کنید و روی Signing & Capabilities کلیک کنید. شناسه بسته‌ای را که اخیراً انتخاب کرده‌اید، در قسمت Team وارد کنید تا تیم خود را تنظیم کنید.

۸۱۲f۹۱۹d۹۶۵c۶۴۹a.jpeg

اکنون می‌توانید Xcode را ببندید و به اندروید استودیو برگردید تا پیکربندی اندروید را تکمیل کنید. برای انجام این کار، فایل build.gradle.kts را در مسیر android/app, باز کنید و applicationId خود (در خط 24 در تصویر زیر) را به شناسه برنامه، همان شناسه بسته iOS، تغییر دهید. توجه داشته باشید که شناسه‌های فروشگاه‌های iOS و اندروید لازم نیست یکسان باشند، با این حال یکسان نگه داشتن آنها احتمال خطا را کاهش می‌دهد و بنابراین در این آزمایشگاه کد نیز از شناسه‌های یکسان استفاده خواهیم کرد.

e320a49ff2068ac2.png

۳. افزونه را نصب کنید

در این بخش از codelab، افزونه‌ی in_app_purchase را نصب خواهید کرد.

اضافه کردن وابستگی در pubspec

با اضافه کردن in_app_purchase به وابستگی‌های پروژه خود، in_app_purchase به pubspec اضافه کنید:

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

pubspec.yaml خود را باز کنید و تأیید کنید که اکنون in_app_purchase به عنوان ورودی در زیر dependencies و in_app_purchase_platform_interface در زیر dev_dependencies فهرست شده‌اند.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

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

۴. اپ استور را راه‌اندازی کنید

برای تنظیم خریدهای درون‌برنامه‌ای و آزمایش آنها در iOS، باید یک برنامه جدید در اپ استور ایجاد کنید و محصولات قابل خرید را در آنجا ایجاد کنید. لازم نیست چیزی منتشر کنید یا برنامه را برای بررسی به اپل ارسال کنید. برای انجام این کار به یک حساب توسعه‌دهنده نیاز دارید. اگر ندارید، در برنامه توسعه‌دهنده اپل ثبت‌نام کنید .

برای استفاده از خریدهای درون‌برنامه‌ای، باید یک توافق‌نامه فعال برای برنامه‌های پولی در App Store Connect داشته باشید. به https://appstoreconnect.apple.com/ بروید و روی توافق‌نامه‌ها، مالیات و بانکداری کلیک کنید.

11db9fca823e7608.png

در اینجا توافق‌نامه‌هایی برای برنامه‌های رایگان و پولی مشاهده خواهید کرد. وضعیت برنامه‌های رایگان باید فعال و وضعیت برنامه‌های پولی جدید باشد. مطمئن شوید که شرایط را مشاهده کرده‌اید، آنها را پذیرفته‌اید و تمام اطلاعات مورد نیاز را وارد کرده‌اید.

74c73197472c9aec.png

وقتی همه چیز به درستی تنظیم شده باشد، وضعیت برنامه‌های پولی فعال خواهد بود. این بسیار مهم است زیرا بدون توافق‌نامه فعال، نمی‌توانید خریدهای درون‌برنامه‌ای را امتحان کنید.

4a100bbb8cafdbbf.jpeg

ثبت شناسه برنامه

یک شناسه جدید در پورتال توسعه‌دهندگان اپل ایجاد کنید. به developer.apple.com/account/resources/identifiers/list مراجعه کنید و روی نماد "به‌علاوه" در کنار سربرگ شناسه‌ها کلیک کنید.

۵۵d7e592d9a3fc7b.png

شناسه‌های برنامه را انتخاب کنید

۱۳f۱۲۵۵۹۸b۷۲ca۷۷.png

برنامه را انتخاب کنید

41ac4c13404e2526.png

توضیحاتی ارائه دهید و شناسه بسته را طوری تنظیم کنید که با شناسه بسته که قبلاً در XCode تنظیم شده بود، مطابقت داشته باشد.

9d2c940ad80deeef.png

برای راهنمایی بیشتر در مورد نحوه ایجاد شناسه برنامه جدید، به راهنمای حساب توسعه‌دهنده مراجعه کنید.

ایجاد یک برنامه جدید

یک برنامه جدید در اپ استور ایجاد کنید. با شناسه بسته منحصر به فرد خود ارتباط برقرار کنید.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

برای راهنمایی بیشتر در مورد نحوه ایجاد یک برنامه جدید و مدیریت توافق‌نامه‌ها، به راهنمای اتصال به فروشگاه App مراجعه کنید.

برای آزمایش خریدهای درون‌برنامه‌ای، به یک کاربر آزمایشی در محیط سندباکس نیاز دارید. این کاربر آزمایشی نباید به iTunes متصل باشد - فقط برای آزمایش خریدهای درون‌برنامه‌ای استفاده می‌شود. نمی‌توانید از آدرس ایمیلی که قبلاً برای یک حساب اپل استفاده شده است استفاده کنید. در بخش کاربران و دسترسی ، برای ایجاد یک حساب سندباکس جدید یا مدیریت اپل آیدی‌های موجود در محیط سندباکس، به سندباکس بروید.

2ba0f599bcac9b36.png

اکنون می‌توانید با رفتن به تنظیمات > توسعه‌دهنده > حساب کاربری اپل در سندباکس، حساب کاربری سندباکس خود را در آیفون تنظیم کنید.

74a545210b282ad8.pngeaa67752f2350f74.png

پیکربندی خریدهای درون‌برنامه‌ای شما

حالا سه مورد قابل خرید را پیکربندی خواهید کرد:

  • dash_consumable_2k : یک خرید مصرفی که می‌تواند بارها خریداری شود و به ازای هر خرید، ۲۰۰۰ دش (واحد پول درون‌برنامه‌ای) به کاربر اعطا می‌کند.
  • dash_upgrade_3d : یک خرید «ارتقاء» غیرقابل مصرف که فقط یک بار می‌توان آن را خریداری کرد و به کاربر یک دش (Dash) با ظاهری متفاوت برای کلیک کردن می‌دهد.
  • dash_subscription_doubler : اشتراکی که در طول مدت اشتراک، به ازای هر کلیک دو برابر Dash به کاربر می‌دهد.

a118161fac83815a.png

به خریدهای درون برنامه‌ای بروید.

خریدهای درون‌برنامه‌ای خود را با شناسه‌های مشخص‌شده ایجاد کنید:

  1. dash_consumable_2k به عنوان یک Consumable تنظیم کنید. dash_consumable_2k به عنوان شناسه محصول استفاده کنید. نام مرجع فقط در App Store Connect استفاده می‌شود، فقط آن را روی dash consumable 2k تنظیم کنید. 1f8527fc03902099.png تنظیم در دسترس بودن. محصول باید در کشور کاربر سندباکس موجود باشد. bd6b2ce2d9314e6e.png قیمت‌گذاری را اضافه کنید و قیمت را روی $1.99 یا معادل آن به ارزهای دیگر تنظیم کنید. 926b03544ae044c4.png برای خرید، محل‌های مورد نظر خود را اضافه کنید. با خرید تماس بگیرید. Spring is in the air 2000 dashes fly out . e26dd4f966dcfece.png یک اسکرین‌شات از نقد و بررسی اضافه کنید. محتوا تا زمانی که محصول برای نقد و بررسی ارسال نشده باشد، مهم نیست، اما لازم است که محصول در حالت «آماده ارسال» باشد، که این حالت برای زمانی که برنامه محصولات را از اپ استور دریافت می‌کند، ضروری است. 25171bfd6f3a033a.png
  2. dash_upgrade_3d به عنوان یک کالای غیر مصرفی تنظیم کنید. از dash_upgrade_3d به عنوان شناسه محصول استفاده کنید. نام مرجع را dash upgrade 3d قرار دهید. خرید را با نام 3D Dash with نامگذاری کنید. این محصول Brings your dash back to the future و به عنوان توضیحات نمایش می‌دهد. قیمت را روی $0.99 تنظیم کنید. موجودی را پیکربندی کنید و تصویر صفحه نقد و بررسی را به همان روشی که برای محصول dash_consumable_2k انجام دادید، آپلود کنید. 83878759f32a7d4a.png
  3. dash_subscription_doubler به عنوان یک اشتراک با قابلیت تمدید خودکار تنظیم کنید. روند اشتراک‌ها کمی متفاوت است. ابتدا، باید یک گروه اشتراک ایجاد کنید. وقتی چندین اشتراک بخشی از یک گروه باشند، یک کاربر فقط می‌تواند همزمان در یکی از آنها مشترک شود، اما می‌تواند بین این اشتراک‌ها ارتقا یا کاهش رتبه دهد. کافیست این گروه را subscriptions بنامید. 393a44b09f3cd8bf.png و محلی‌سازی را برای گروه اشتراک اضافه کنید. 595aa910776349bd.png در مرحله بعد، اشتراک را ایجاد خواهید کرد. نام مرجع را روی dash subscription doubler و شناسه محصول را روی dash_subscription_doubler تنظیم کنید. 7bfff7bbe11c8eec.png سپس، مدت اشتراک ۱ هفته و محلی‌سازی‌ها را انتخاب کنید. نام این اشتراک را Jet Engine با توضیحات Doubles your clicks بگذارید. قیمت را روی $0.49 تنظیم کنید. در دسترس بودن را پیکربندی کنید و تصویر نقد و بررسی را به همان روشی که برای محصول dash_consumable_2k انجام دادید، آپلود کنید. ۴۴d18e02b926a334.png

اکنون باید محصولات موجود در لیست‌ها را مشاهده کنید:

۱۷f۲۴۲b۵c۱۴۲۶b۷۹.pngd71da951f595054a.png

۵. فروشگاه Play را راه‌اندازی کنید

همانند اپ استور، برای استفاده از پلی استور نیز به یک حساب توسعه‌دهنده نیاز دارید. اگر هنوز حساب کاربری ندارید، یک حساب کاربری ثبت کنید .

یک برنامه جدید ایجاد کنید

یک برنامه جدید در کنسول گوگل پلی ایجاد کنید:

  1. کنسول Play را باز کنید.
  2. همه برنامه‌ها > ایجاد برنامه را انتخاب کنید.
  3. یک زبان پیش‌فرض انتخاب کنید و یک عنوان برای برنامه خود اضافه کنید. نام برنامه خود را همانطور که می‌خواهید در گوگل پلی نمایش داده شود، تایپ کنید. می‌توانید بعداً نام را تغییر دهید.
  4. مشخص کنید که برنامه شما یک بازی است. می‌توانید بعداً این را تغییر دهید.
  5. مشخص کنید که آیا برنامه شما رایگان است یا پولی.
  6. دستورالعمل‌های محتوا و اعلامیه‌های قوانین صادرات ایالات متحده را تکمیل کنید.
  7. ایجاد برنامه را انتخاب کنید.

پس از ایجاد برنامه، به داشبورد بروید و تمام وظایف موجود در بخش «راه‌اندازی برنامه» را انجام دهید. در اینجا، اطلاعاتی در مورد برنامه خود، مانند رتبه‌بندی محتوا و تصاویر صفحه، ارائه می‌دهید. ۱۳۸۴۵badcf9bc1db.png

درخواست را امضا کنید

برای اینکه بتوانید خریدهای درون‌برنامه‌ای را آزمایش کنید، حداقل به یک نسخه آپلود شده در گوگل پلی نیاز دارید.

برای این کار، لازم است نسخه آزمایشی شما با چیزی غیر از کلیدهای اشکال‌زدایی امضا شده باشد.

ایجاد یک فروشگاه کلید

اگر از قبل یک keystore دارید، به مرحله بعدی بروید. در غیر این صورت، با اجرای دستور زیر در خط فرمان، یکی ایجاد کنید.

در مک/لینوکس، از دستور زیر استفاده کنید:

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

در ویندوز، از دستور زیر استفاده کنید:

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

این دستور فایل key.jks را در دایرکتوری خانگی شما ذخیره می‌کند. اگر می‌خواهید فایل را در جای دیگری ذخیره کنید، آرگومانی که به پارامتر -keystore می‌دهید را تغییر دهید.

keystore

فایل خصوصی است؛ آن را در کنترل منبع عمومی بررسی نکنید!

مرجع کلید اصلی از برنامه

فایلی با نام <your app dir>/android/key.properties ایجاد کنید که حاوی ارجاعی به keystore شما باشد:

storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>

پیکربندی امضا در Gradle

با ویرایش فایل <your app dir>/android/app/build.gradle.kts امضای برنامه خود را پیکربندی کنید.

اطلاعات keystore را از فایل properties خود قبل از بلوک android اضافه کنید:

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

plugins {
    // omitted
}

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

android {
    // omitted
}

فایل key.properties را در شیء keystoreProperties بارگذاری کنید.

بلوک buildTypes را به صورت زیر به‌روزرسانی کنید:

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

بلوک signingConfigs را در فایل build.gradle.kts ماژول خود با اطلاعات پیکربندی امضا پیکربندی کنید:

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

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

نسخه‌های منتشر شده از برنامه شما اکنون به طور خودکار امضا می‌شوند.

برای اطلاعات بیشتر در مورد امضای برنامه، به امضای برنامه در developer.android.com مراجعه کنید.

اولین ساخته خود را آپلود کنید

پس از اینکه برنامه شما برای امضا پیکربندی شد، باید بتوانید برنامه خود را با اجرای دستور زیر بسازید:

flutter build appbundle

این دستور به طور پیش‌فرض یک نسخه آزمایشی (release build) ایجاد می‌کند و خروجی آن را می‌توانید در <your app dir>/build/app/outputs/bundle/release/ بیابید.

از داشبورد کنسول گوگل پلی، به مسیر Test and release > Testing > Closed testing بروید و یک نسخه آزمایشی بسته جدید ایجاد کنید.

در مرحله بعد، بسته برنامه app-release.aab را که توسط دستور build ایجاد شده است، آپلود کنید.

روی ذخیره کلیک کنید و سپس روی بررسی انتشار کلیک کنید.

در نهایت، برای فعال کردن نسخه آزمایشی بسته، روی شروع انتشار به آزمایش بسته کلیک کنید.

تنظیم کاربران آزمایشی

برای اینکه بتوانید خریدهای درون‌برنامه‌ای را آزمایش کنید، باید حساب‌های گوگل آزمایش‌کنندگان شما در دو مکان به کنسول گوگل پلی اضافه شوند:

  1. به مسیر آزمایشی خاص (آزمایش داخلی)
  2. به عنوان آزمایش کننده مجوز

ابتدا، با اضافه کردن تستر به مسیر تست داخلی شروع کنید. به مسیر تست و انتشار > تست > تست داخلی برگردید و روی تب تسترها کلیک کنید.

a0d0394e85128f84.png

با کلیک روی «ایجاد لیست ایمیل»، یک لیست ایمیل جدید ایجاد کنید. برای لیست یک نام انتخاب کنید و آدرس‌های ایمیل حساب‌های گوگلی که نیاز به دسترسی به خریدهای درون‌برنامه‌ای آزمایشی دارند را اضافه کنید.

سپس، کادر انتخاب مربوط به لیست را علامت بزنید و روی ذخیره تغییرات کلیک کنید.

سپس، آزمایش‌کنندگان مجوز را اضافه کنید:

  1. به نمای «همه برنامه‌ها» در کنسول گوگل پلی برگردید.
  2. به تنظیمات > آزمایش مجوز بروید.
  3. همان آدرس‌های ایمیل آزمایش‌کنندگانی را که باید بتوانند خریدهای درون‌برنامه‌ای را آزمایش کنند، اضافه کنید.
  4. پاسخ لایسنس را روی RESPOND_NORMALLY تنظیم کنید.
  5. روی ذخیره تغییرات کلیک کنید.

a1a0f9d3e55ea8da.png

پیکربندی خریدهای درون‌برنامه‌ای شما

اکنون مواردی را که می‌توان از طریق برنامه خریداری کرد، پیکربندی خواهید کرد.

درست مانند اپ استور، باید سه خرید مختلف را تعریف کنید:

  • dash_consumable_2k : یک خرید مصرفی که می‌تواند بارها خریداری شود و به ازای هر خرید، ۲۰۰۰ دش (واحد پول درون‌برنامه‌ای) به کاربر اعطا می‌کند.
  • dash_upgrade_3d : یک خرید «ارتقاء» غیرقابل مصرف که فقط یک بار می‌توان آن را خریداری کرد و به کاربر یک دش (Dash) با ظاهری متفاوت برای کلیک کردن می‌دهد.
  • dash_subscription_doubler : اشتراکی که در طول مدت اشتراک، به ازای هر کلیک دو برابر Dash به کاربر می‌دهد.

ابتدا، اقلام مصرفی و غیر مصرفی را اضافه کنید.

  1. به کنسول گوگل پلی بروید و برنامه مورد نظر خود را انتخاب کنید.
  2. به کسب درآمد > محصولات > محصولات درون برنامه‌ای بروید.
  3. روی ایجاد محصول کلیک کنید c8d66e32f57dee21.png
  4. تمام اطلاعات مورد نیاز برای محصول خود را وارد کنید. مطمئن شوید که شناسه محصول با شناسه‌ای که قصد استفاده از آن را دارید، دقیقاً مطابقت دارد.
  5. روی ذخیره کلیک کنید.
  6. روی فعال کردن کلیک کنید.
  7. این فرآیند را برای خرید «ارتقاء» کالای غیرمصرفی تکرار کنید.

سپس، اشتراک را اضافه کنید:

  1. به کنسول گوگل پلی بروید و برنامه مورد نظر خود را انتخاب کنید.
  2. به کسب درآمد > محصولات > اشتراک‌ها بروید.
  3. روی ایجاد اشتراک کلیک کنید 32a6a9eefdb71dd0.png
  4. تمام اطلاعات مورد نیاز برای اشتراک خود را وارد کنید. مطمئن شوید که شناسه محصول دقیقاً با شناسه‌ای که قصد استفاده از آن را دارید، مطابقت دارد.
  5. روی ذخیره کلیک کنید

اکنون خریدهای شما باید در کنسول Play تنظیم شده باشند.

۶. فایربیس را راه‌اندازی کنید

در این آزمایشگاه کد، شما از یک سرویس backend برای تأیید و پیگیری خریدهای کاربران استفاده خواهید کرد.

استفاده از سرویس backend مزایای متعددی دارد:

  • شما می‌توانید تراکنش‌ها را به صورت امن تأیید کنید.
  • می‌توانید به رویدادهای صورتحساب از فروشگاه‌های برنامه واکنش نشان دهید.
  • شما می‌توانید خریدها را در یک پایگاه داده پیگیری کنید.
  • کاربران نمی‌توانند با تنظیم مجدد ساعت سیستم خود، برنامه شما را فریب دهند تا ویژگی‌های پریمیوم را ارائه دهد.

در حالی که روش‌های زیادی برای راه‌اندازی یک سرویس backend وجود دارد، شما این کار را با استفاده از توابع ابری و Firestore و با استفاده از Firebase خود گوگل انجام خواهید داد.

نوشتن بخش بک‌اند خارج از محدوده‌ی این آزمایشگاه کد در نظر گرفته می‌شود، بنابراین کد اولیه از قبل شامل یک پروژه‌ی فایربیس است که خریدهای اولیه برای شروع کار را مدیریت می‌کند.

افزونه‌های فایربیس نیز در برنامه‌ی آغازین گنجانده شده‌اند.

کاری که باقی مانده این است که پروژه Firebase خود را ایجاد کنید، هم برنامه و هم backend را برای Firebase پیکربندی کنید و در نهایت backend را مستقر کنید.

ایجاد یک پروژه فایربیس

به کنسول Firebase بروید و یک پروژه Firebase جدید ایجاد کنید. برای این مثال، نام پروژه را Dash Clicker بگذارید.

در برنامه‌ی بک‌اند، شما خریدها را به یک کاربر خاص مرتبط می‌کنید، بنابراین به احراز هویت نیاز دارید. برای این کار، از ماژول احراز هویت فایربیس با ورود به سیستم گوگل استفاده کنید.

  1. از داشبورد Firebase، به Authentication بروید و در صورت نیاز آن را فعال کنید.
  2. به برگه «روش ورود» بروید و ارائه‌دهنده ورود به سیستم گوگل را فعال کنید.

fe2e0933d6810888.png

از آنجا که از پایگاه داده Firestore فایربیس نیز استفاده خواهید کرد، این را نیز فعال کنید.

d02d641821c71e2c.png

قوانین Cloud Firestore را به این صورت تنظیم کنید:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /purchases/{purchaseId} {
      allow read: if request.auth != null && request.auth.uid == resource.data.userId
    }
  }
}

راه‌اندازی فایربیس برای فلاتر

روش پیشنهادی برای نصب Firebase روی برنامه Flutter استفاده از FlutterFire CLI است. دستورالعمل‌های توضیح داده شده در صفحه تنظیمات را دنبال کنید.

هنگام اجرای flutterfire configure، پروژه‌ای را که در مرحله قبل ایجاد کرده‌اید، انتخاب کنید.

$ flutterfire configure

i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
  other-flutter-codelab-1 (other-flutter-codelab-1)
  other-flutter-codelab-2 (other-flutter-codelab-2)
  other-flutter-codelab-3 (other-flutter-codelab-3)
  other-flutter-codelab-4 (other-flutter-codelab-4)
  <create a new project>

در مرحله بعد، با انتخاب دو پلتفرم ، iOS و اندروید را فعال کنید.

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

وقتی از شما در مورد لغو firebase_options.dart سوال شد، بله را انتخاب کنید.

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

راه‌اندازی فایربیس برای اندروید: مراحل بعدی

از داشبورد فایربیس، به نمای کلی پروژه بروید، تنظیمات را انتخاب کنید و سپس تب عمومی (General ) را انتخاب کنید.

به پایین اسکرول کنید تا به برنامه‌های شما (Your apps) برسید و برنامه‌ی dashclicker (android) را انتخاب کنید.

b22d46a759c0c834.png

برای اینکه بتوانید در حالت اشکال‌زدایی با گوگل وارد سیستم شوید، باید اثر انگشت هش SHA-1 مربوط به گواهی اشکال‌زدایی خود را ارائه دهید.

هش گواهی امضای اشکال‌زدایی خود را دریافت کنید

در ریشه پروژه برنامه Flutter خود، دایرکتوری را به پوشه android/ تغییر دهید و سپس یک گزارش امضا ایجاد کنید.

cd android
./gradlew :app:signingReport

لیست بزرگی از کلیدهای امضا به شما نمایش داده می‌شود. از آنجایی که به دنبال هش مربوط به گواهی اشکال‌زدایی هستید، به دنبال گواهی‌ای باشید که ویژگی‌های Variant و Config روی debug تنظیم شده باشد. احتمالاً keystore در پوشه home شما در مسیر .android/debug.keystore ‎ قرار دارد.

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

هش SHA-1 را کپی کنید و آخرین فیلد را در پنجره‌ی محاوره‌ای ارسال برنامه پر کنید.

در نهایت، دستور flutterfire configure را دوباره اجرا کنید تا برنامه به‌روزرسانی شود و پیکربندی امضا را نیز شامل شود.

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

راه‌اندازی فایربیس برای iOS: مراحل بعدی

فضای ios/Runner.xcworkspace را با Xcode یا با IDE مورد نظر خود باز کنید.

در VSCode روی پوشه ios/ کلیک راست کرده و سپس open in xcode .

در اندروید استودیو، روی پوشه ios/ کلیک راست کنید، سپس روی flutter و سپس گزینه open iOS module in Xcode کلیک کنید.

برای فعال کردن ورود با گوگل در iOS، گزینه پیکربندی CFBundleURLTypes را به فایل‌های plist ساخت خود اضافه کنید. (برای اطلاعات بیشتر، مستندات بسته google_sign_in را بررسی کنید.) در این مورد، فایل ios/Runner/Info.plist است.

جفت کلید-مقدار قبلاً اضافه شده است، اما مقادیر آنها باید جایگزین شوند:

  1. مقدار REVERSED_CLIENT_ID را از فایل GoogleService-Info.plist ، بدون عنصر <string>..</string> اطراف آن، دریافت کنید.
  2. مقدار را در فایل ios/Runner/Info.plist خود، زیر کلید CFBundleURLTypes جایگزین کنید.
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.REDACTED</string>
        </array>
    </dict>
</array>

اکنون کار شما با راه‌اندازی Firebase تمام شده است.

۷. به به‌روزرسانی‌های خرید گوش دهید

در این بخش از آزمایشگاه کد، برنامه را برای خرید محصولات آماده خواهید کرد. این فرآیند شامل گوش دادن به به‌روزرسانی‌های خرید و خطاها پس از شروع برنامه است.

به به‌روزرسانی‌های خرید گوش دهید

در main.dart, ویجت MyHomePage را پیدا کنید که دارای یک Scaffold با BottomNavigationBar است که شامل دو صفحه می‌باشد. این صفحه همچنین سه Provider برای DashCounter ، DashUpgrades, و DashPurchases ایجاد می‌کند. DashCounter تعداد فعلی Dashes را پیگیری کرده و به طور خودکار آنها را افزایش می‌دهد. DashUpgrades ارتقاءهایی را که می‌توانید با Dashes خریداری کنید، مدیریت می‌کند. این آزمایشگاه کد بر DashPurchases تمرکز دارد.

به طور پیش‌فرض، شیء یک ارائه‌دهنده زمانی تعریف می‌شود که آن شیء برای اولین بار درخواست شود. این شیء مستقیماً هنگام شروع برنامه به خرید به‌روزرسانی‌ها گوش می‌دهد، بنابراین بارگذاری تنبل را روی این شیء با lazy: false غیرفعال کنید:

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create: (context) => DashPurchases(
    context.read<DashCounter>(),
  ),
  lazy: false,                                             // Add this line
),

شما همچنین به یک نمونه از InAppPurchaseConnection نیاز دارید. با این حال، برای اینکه برنامه قابل آزمایش باشد، به روشی برای شبیه‌سازی اتصال نیاز دارید. برای انجام این کار، یک متد نمونه ایجاد کنید که بتواند در تست لغو شود و آن را به main.dart اضافه کنید.

lib/main.dart

// Gives the option to override in tests.
class IAPConnection {
  static InAppPurchase? _instance;
  static set instance(InAppPurchase value) {
    _instance = value;
  }

  static InAppPurchase get instance {
    _instance ??= InAppPurchase.instance;
    return _instance!;
  }
}

تست را به صورت زیر به‌روزرسانی کنید:

تست/widget_test.dart

import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import

void main() {
  testWidgets('App starts', (tester) async {
    IAPConnection.instance = TestIAPConnection();          // Add this line
    await tester.pumpWidget(const MyApp());
    expect(find.text('Tim Sneath'), findsOneWidget);
  });
}

class TestIAPConnection implements InAppPurchase {         // Add from here
  @override
  Future<bool> buyConsumable({
    required PurchaseParam purchaseParam,
    bool autoConsume = true,
  }) {
    return Future.value(false);
  }

  @override
  Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
    return Future.value(false);
  }

  @override
  Future<void> completePurchase(PurchaseDetails purchase) {
    return Future.value();
  }

  @override
  Future<bool> isAvailable() {
    return Future.value(false);
  }

  @override
  Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
    return Future.value(
      ProductDetailsResponse(productDetails: [], notFoundIDs: []),
    );
  }

  @override
  T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
    // TODO: implement getPlatformAddition
    throw UnimplementedError();
  }

  @override
  Stream<List<PurchaseDetails>> get purchaseStream =>
      Stream.value(<PurchaseDetails>[]);

  @override
  Future<void> restorePurchases({String? applicationUserName}) {
    // TODO: implement restorePurchases
    throw UnimplementedError();
  }

  @override
  Future<String> countryCode() {
    // TODO: implement countryCode
    throw UnimplementedError();
  }
}                                                          // To here.

در lib/logic/dash_purchases.dart ، به کد مربوط به DashPurchasesChangeNotifier بروید. در این مرحله، فقط یک DashCounter وجود دارد که می‌توانید به Dashهای خریداری شده خود اضافه کنید.

یک ویژگی اشتراک جریان، _subscription (از نوع StreamSubscription<List<PurchaseDetails>> _subscription; )، نمونه IAPConnection.instance, و importها اضافه کنید. کد حاصل باید به شکل زیر باشد:

lib/logic/dash_purchases.dart

import 'dart:async';

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

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

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

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;                  // And this line

  DashPurchases(this.counter);

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
}

کلمه کلیدی late به _subscription اضافه شده است زیرا _subscription در سازنده مقداردهی اولیه شده است. این پروژه به طور پیش‌فرض طوری تنظیم شده است که nullable نباشد (NNBD)، به این معنی که ویژگی‌هایی که nullable اعلام نشده‌اند باید مقداری غیر null داشته باشند. تعریف late به شما امکان می‌دهد تعریف این مقدار را به تأخیر بیندازید.

در سازنده، جریان purchaseUpdated را دریافت کرده و شروع به گوش دادن به جریان کنید. در متد dispose() ، اشتراک جریان را لغو کنید.

lib/logic/dash_purchases.dart

import 'dart:async';

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

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

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

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

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

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

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

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

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

اکنون، برنامه به‌روزرسانی‌های خرید را دریافت می‌کند، بنابراین، در بخش بعدی، شما خریدی انجام خواهید داد!

قبل از ادامه، تست‌ها را با استفاده از « flutter test" اجرا کنید تا مطمئن شوید همه چیز به درستی تنظیم شده است.

$ flutter test

00:01 +1: All tests passed!

۸. خرید کنید

در این بخش از آزمایشگاه کد، محصولات آزمایشی موجود را با محصولات قابل خرید واقعی جایگزین خواهید کرد. این محصولات از فروشگاه‌ها بارگیری می‌شوند، در یک لیست نشان داده می‌شوند و با لمس محصول خریداری می‌شوند.

محصول قابل خرید تطبیقی

PurchasableProduct یک محصول آزمایشی (mock product) نمایش می‌دهد. با جایگزینی کلاس PurchasableProduct در purchasable_product.dart با کد زیر، آن را به‌روزرسانی کنید تا محتوای واقعی را نشان دهد:

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus { purchasable, purchased, pending }

class PurchasableProduct {
  String get id => productDetails.id;
  String get title => productDetails.title;
  String get description => productDetails.description;
  String get price => productDetails.price;
  ProductStatus status;
  ProductDetails productDetails;

  PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}

در dash_purchases.dart, خریدهای ساختگی را حذف کرده و آنها را با یک لیست خالی جایگزین کنید، List<PurchasableProduct> products = []; .

خریدهای موجود را بارگیری کنید

برای اینکه به کاربر امکان خرید بدهید، خریدهای فروشگاه را بارگذاری کنید. ابتدا بررسی کنید که آیا فروشگاه در دسترس است یا خیر. وقتی فروشگاه در دسترس نباشد، تنظیم storeState روی notAvailable یک پیام خطا به کاربر نمایش می‌دهد.

lib/logic/dash_purchases.dart

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
  }

وقتی فروشگاه در دسترس قرار گرفت، خریدهای موجود را بارگذاری کنید. با توجه به تنظیمات قبلی Google Play و App Store، انتظار داشته باشید storeKeyConsumable ، storeKeySubscription, و storeKeyUpgrade ببینید. وقتی خرید مورد انتظار در دسترس نیست، این اطلاعات را در کنسول چاپ کنید؛ همچنین ممکن است بخواهید این اطلاعات را به سرویس backend ارسال کنید.

متد await iapConnection.queryProductDetails(ids) هم شناسه‌هایی که پیدا نشده‌اند و هم محصولات قابل خریدی که پیدا شده‌اند را برمی‌گرداند. از productDetails از پاسخ برای به‌روزرسانی رابط کاربری استفاده کنید و StoreState را روی available تنظیم کنید.

lib/logic/dash_purchases.dart

import '../constants.dart';

// ...

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    products = response.productDetails
        .map((e) => PurchasableProduct(e))
        .toList();
    storeState = StoreState.available;
    notifyListeners();
  }

تابع loadPurchases() را در سازنده فراخوانی کنید:

lib/logic/dash_purchases.dart

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

در نهایت، مقدار فیلد storeState را از StoreState.available به StoreState.loading:

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

نمایش محصولات قابل خرید

فایل purchase_page.dart در نظر بگیرید. ویجت PurchasePage بسته به StoreState _PurchasesLoading ، _PurchaseList, یا _PurchasesNotAvailable, را نشان می‌دهد. این ویجت همچنین خریدهای قبلی کاربر را که در مرحله بعدی استفاده می‌شود، نشان می‌دهد.

ویجت _PurchaseList فهرست محصولات قابل خرید را نشان می‌دهد و یک درخواست خرید به شیء DashPurchases ارسال می‌کند.

lib/pages/purchase_page.dart

class _PurchaseList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var purchases = context.watch<DashPurchases>();
    var products = purchases.products;
    return Column(
      children: products
          .map(
            (product) => _PurchaseWidget(
              product: product,
              onPressed: () {
                purchases.buy(product);
              },
            ),
          )
          .toList(),
    );
  }
}

اگر فروشگاه‌های اندروید و iOS به درستی پیکربندی شده باشند، باید بتوانید محصولات موجود در آن‌ها را ببینید. توجه داشته باشید که هنگام ورود به کنسول‌های مربوطه، ممکن است مدتی طول بکشد تا خریدها در دسترس قرار گیرند.

ca1a9f97c21e552d.png

به dash_purchases.dart برگردید و تابع خرید یک محصول را پیاده‌سازی کنید. شما فقط باید اقلام مصرفی را از اقلام غیر مصرفی جدا کنید. محصولات ارتقاء و اشتراک، اقلام غیر مصرفی هستند.

lib/logic/dash_purchases.dart

  Future<void> buy(PurchasableProduct product) async {
    final purchaseParam = PurchaseParam(productDetails: product.productDetails);
    switch (product.id) {
      case storeKeyConsumable:
        await iapConnection.buyConsumable(purchaseParam: purchaseParam);
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
      default:
        throw ArgumentError.value(
          product.productDetails,
          '${product.id} is not a known product',
        );
    }
  }

قبل از ادامه، متغیر _beautifiedDashUpgrade را ایجاد کنید و getter beautifiedDash را برای ارجاع به آن به‌روزرسانی کنید.

lib/logic/dash_purchases.dart

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

متد _onPurchaseUpdate به‌روزرسانی‌های خرید را دریافت می‌کند، وضعیت محصول نمایش داده شده در صفحه خرید را به‌روزرسانی می‌کند و خرید را در منطق شمارنده اعمال می‌کند. فراخوانی تابع completePurchase پس از مدیریت خرید بسیار مهم است تا فروشگاه بداند که خرید به درستی مدیریت شده است.

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
    List<PurchaseDetails> purchaseDetailsList,
  ) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
      }
    }

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

۹. تنظیمات بک‌اند

قبل از اینکه به سراغ ردیابی و تأیید خریدها بروید، یک backend دارت برای پشتیبانی از انجام این کار راه‌اندازی کنید.

در این بخش، از پوشه‌ی dart-backend/ به عنوان ریشه استفاده کنید.

مطمئن شوید که ابزارهای زیر را نصب کرده‌اید:

نمای کلی پروژه پایه

از آنجا که برخی از بخش‌های این پروژه خارج از محدوده این آزمایشگاه کد در نظر گرفته می‌شوند، در کد اولیه گنجانده شده‌اند. ایده خوبی است که قبل از شروع، آنچه را که از قبل در کد اولیه وجود دارد، مرور کنید تا ایده‌ای از نحوه ساختاردهی امور داشته باشید.

این کد backend می‌تواند به صورت محلی روی دستگاه شما اجرا شود، برای استفاده از آن نیازی به استقرار آن ندارید. با این حال، باید بتوانید از دستگاه توسعه خود (اندروید یا آیفون) به دستگاهی که سرور روی آن اجرا خواهد شد متصل شوید. برای این کار، آنها باید در یک شبکه باشند و شما باید آدرس IP دستگاه خود را بدانید.

سعی کنید سرور را با استفاده از دستور زیر اجرا کنید:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

بک‌اند دارت از shelf و shelf_router برای سرویس‌دهی به نقاط انتهایی API استفاده می‌کند. به طور پیش‌فرض، سرور هیچ مسیری ارائه نمی‌دهد. بعداً شما یک مسیر برای مدیریت فرآیند تأیید خرید ایجاد خواهید کرد.

یکی از بخش‌هایی که از قبل در کد اولیه گنجانده شده است، IapRepository در lib/iap_repository.dart است. از آنجا که یادگیری نحوه تعامل با Firestore یا پایگاه‌های داده به طور کلی، به این آزمایشگاه کد مربوط نمی‌شود، کد اولیه شامل توابعی برای ایجاد یا به‌روزرسانی خریدها در Firestore و همچنین تمام کلاس‌های مربوط به آن خریدها است.

تنظیم دسترسی فایربیس

برای دسترسی به Firebase Firestore، به یک کلید دسترسی حساب سرویس نیاز دارید. با باز کردن تنظیمات پروژه Firebase، یکی ایجاد کنید و به بخش حساب‌های سرویس بروید، سپس گزینه Generate new private key را انتخاب کنید.

۲۷۵۹۰fc۷۷ae۹۴ad۴.png

فایل JSON دانلود شده را در پوشه assets/ کپی کنید و نام آن را به service-account-firebase.json تغییر دهید.

تنظیم دسترسی به گوگل پلی

برای دسترسی به فروشگاه Play جهت تأیید خریدها، باید یک حساب کاربری سرویس با این مجوزها ایجاد کنید و اعتبارنامه‌های JSON مربوط به آن را دانلود کنید.

  1. از صفحه API توسعه‌دهندگان اندروید گوگل پلی در کنسول ابری گوگل دیدن کنید. 629f0bd8e6b50be8.png در صورتی که کنسول گوگل پلی از شما درخواست ایجاد یا پیوند به یک پروژه موجود را داشته باشد، ابتدا این کار را انجام دهید و سپس به این صفحه برگردید.
  2. سپس، به صفحه حساب‌های سرویس بروید و روی + ایجاد حساب سرویس کلیک کنید. 8dc97e3b1262328a.png
  3. نام حساب سرویس را وارد کنید و روی ایجاد و ادامه کلیک کنید. 4fe8106af85ce75f.png
  4. نقش Pub/Sub Subscriber را انتخاب کنید و روی Done کلیک کنید. a5b6fa6ea8ee22d.png
  5. پس از ایجاد حساب کاربری، به بخش مدیریت کلیدها (Manage keys) بروید. eb36da2c1ad6dd06.png
  6. افزودن کلید > ایجاد کلید جدید را انتخاب کنید. e92db9557a28a479.png
  7. یک کلید JSON ایجاد و دانلود کنید. 711d04f2f4176333.png
  8. فایل دانلود شده را به service-account-google-play.json, تغییر نام دهید و آن را به دایرکتوری assets/ منتقل کنید.
  9. سپس، به صفحه کاربران و مجوزها در کنسول Play بروید ۲۸fffbfc35b45f97.png
  10. روی دعوت از کاربران جدید کلیک کنید و آدرس ایمیل حساب سرویس ایجاد شده قبلی را وارد کنید. می‌توانید ایمیل را در جدول صفحه حساب‌های سرویس پیدا کنید. e3310cc077f397d.png
  11. مجوزهای مشاهده داده‌های مالی و مدیریت سفارشات و اشتراک‌ها را برای برنامه اعطا کنید. a3b8cf2b660d1900.png
  12. روی دعوت از کاربر کلیک کنید.

یک کار دیگر که باید انجام دهیم این است که lib/constants.dart, را باز کنیم و مقدار androidPackageId را با شناسه بسته‌ای که برای برنامه اندروید خود انتخاب کرده‌اید، جایگزین کنیم.

تنظیم دسترسی به اپ استور اپل

برای دسترسی به اپ استور جهت تأیید خریدها، باید یک راز مشترک تنظیم کنید:

  1. اتصال به اپ استور را باز کنید.
  2. Go to My Apps, and select your app.
  3. In the sidebar navigation, go to General > App information .
  4. Click Manage under App-Specific Shared Secret header. ad419782c5fbacb2.png
  5. Generate a new secret, and copy it. b5b72a357459b0e5.png
  6. Open lib/constants.dart, and replace the value of appStoreSharedSecret with the shared secret you just generated.

Constants configuration file

Before proceeding, make sure that the following constants are configured in the lib/constants.dart file:

  • androidPackageId : Package ID used on Android, such as com.example.dashclicker
  • appStoreSharedSecret : Shared secret to access App Store Connect to perform purchase verification.
  • bundleId : Bundle ID used on iOS, such as com.example.dashclicker

You can ignore the rest of the constants for the time being.

10. Verify purchases

The general flow for verifying purchases is similar for iOS and Android.

For both stores, your application receives a token when a purchase is made.

This token is sent by the app to your backend service, which then, in turn, verifies the purchase with the respective store's servers using the provided token.

The backend service can then choose to store the purchase, and reply to the application whether the purchase was valid or not.

By having the backend service do the validation with the stores rather than the application running on your user's device, you can prevent the user gaining access to premium features by, for example, rewinding their system clock.

Set up the Flutter side

Set up authentication

As you are going to send the purchases to your backend service, you want to make sure the user is authenticated while making a purchase. Most of the authentication logic is already added for you in the starter project, you just have to make sure the PurchasePage shows the login button when the user is not logged in yet. Add the following code to the beginning of the build method of 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.

    // ...

Call verification endpoint from the app

In the app, create the _verifyPurchase(PurchaseDetails purchaseDetails) function that calls the /verifypurchase endpoint on your Dart backend using an http post call.

Send the selected store ( google_play for the Play Store or app_store for the App Store), the serverVerificationData , and the productID . The server returns status code indicating whether the purchase is verified.

In the app constants, configure the server IP to your local machine IP address.

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

Add the firebaseNotifier with the creation of DashPurchases in main.dart:

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
          ),
          lazy: false,
        ),

Add a getter for the User in the FirebaseNotifier, so you can pass the user ID to the verify purchase function.

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 {
    // ...

Add the function _verifyPurchase to the DashPurchases class. This async function returns a boolean indicating whether the purchase is validated.

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

Call the _verifyPurchase function in _handlePurchase just before you apply the purchase. You should only apply the purchase when it's verified. In a production app, you can specify this further to, for example, apply a trial subscription when the store is temporarily unavailable. However, for this example, apply the purchase when the purchase is verified successfully.

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

In the app everything is now ready to validate the purchases.

Set up the backend service

Next, set up the backend for verifying purchases on the backend.

Build purchase handlers

Because the verification flow for both stores is close to identical, set up an abstract PurchaseHandler class with separate implementations for each store.

be50c207c5a2a519.png

Start by adding a purchase_handler.dart file to the lib/ folder, where you define an abstract PurchaseHandler class with two abstract methods for verifying two different kinds of purchases: subscriptions and non-subscriptions.

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

As you can see, each method requires three parameters:

  • userId: The ID of the logged-in user, so you can tie purchases to the user.
  • productData: Data about the product. You are going to define this in a minute.
  • token: The token provided to the user by the store.

Additionally, to make these purchase handlers easier to use, add a verifyPurchase() method that can be used for both subscriptions and non-subscriptions:

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

Now, you can just call verifyPurchase for both cases, but still have separate implementations!

The ProductData class contains basic information about the different purchasable products, which includes the product ID (sometimes also referred to as SKU) and the ProductType .

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

  const ProductData(this.productId, this.type);
}

The ProductType can either be a subscription or a non-subscription.

lib/products.dart

enum ProductType { subscription, nonSubscription }

Finally, the list of products is defined as a map in the same file.

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

Next, define some placeholder implementations for the Google Play Store and the Apple App Store. Start with Google Play:

Create lib/google_play_purchase_handler.dart , and add a class that extends the PurchaseHandler you just wrote:

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

For now, it returns true for the handler methods; you'll get to them later.

As you might have noticed, the constructor takes an instance of the IapRepository . The purchase handler uses this instance to store information about purchases in Firestore later on. To communicate with Google Play, you use the provided AndroidPublisherApi .

Next, do the same for the app store handler. Create lib/app_store_purchase_handler.dart , and add a class that extends the PurchaseHandler again:

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

Great! Now you have two purchase handlers. Next, create the purchase verification API endpoint.

Use purchase handlers

Open bin/server.dart and create an API endpoint using 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');
  }
}

The code is doing the following:

  1. Define a POST endpoint that will be called from the app you created previously.
  2. Decode the JSON payload and extract the following information:
    1. userId : Logged in user ID
    2. source : Store used, either app_store or google_play .
    3. productData : Obtained from the productDataMap you created previously.
    4. token : Contains the verification data to send to the stores.
  3. Call to the verifyPurchase method, either for the GooglePlayPurchaseHandler or the AppStorePurchaseHandler , depending on the source.
  4. If the verification was successful, the method returns a Response.ok to the client.
  5. If the verification fails, the method returns a Response.internalServerError to the client.

After creating the API endpoint, you need to configure the two purchase handlers. This requires you to load the service account keys you obtained in the previous step and configure the access to the different services, including the Android Publisher API and the Firebase Firestore API. Then, create the two purchase handlers with the different dependencies:

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

Verify Android purchases: Implement the purchase hander

Next, continue implementing the Google Play purchase handler.

Google already provides Dart packages for interacting with the APIs you need to verify purchases. You initialized them in the server.dart file and now use them in the GooglePlayPurchaseHandler class.

Implement the handler for non-subscription-type purchases:

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

You can update the subscription purchase handler in a similar way:

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

Add the following method to facilitate the parsing of order IDs, as well as two methods to parse the purchase status.

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

Your Google Play purchases should now be verified and stored in the database.

Next, move on to App Store purchases for iOS.

Verify iOS purchases: Implement the purchase handler

For verifying purchases with the App Store, a third-party Dart package exists named app_store_server_sdk that makes the process easier.

Start by creating the ITunesApi instance. Use the sandbox configuration, as well as enable logging to facilitate error debugging.

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(ITunesEnvironment.sandbox(), loggingEnabled: true),
  );

Now, unlike the Google Play APIs, the App Store uses the same API endpoints for both subscriptions and non-subscriptions. This means that you can use the same logic for both handlers. Merge them together so they call the same implementation:

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
  }

Now, implement 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;
    }
  }

Your App Store purchases should now be verified and stored in the database!

Run the backend

At this point, you can run dart bin/server.dart to serve the /verifypurchase endpoint.

$ dart bin/server.dart
Serving at http://0.0.0.0:8080

11. Keep track of purchases

The recommended way to track your users' purchases is in the backend service. This is because your backend can respond to events from the store and thus is less prone to running into outdated information due to caching, as well as being less susceptible to being tampered with.

First, set up the processing of store events on the backend with the Dart backend you've been building.

Process store events on the backend

Stores have the ability to inform your backend of any billing events that happen, such as when subscriptions renew. You can process these events in your backend to keep the purchases in your database current. In this section, set this up for both the Google Play Store and the Apple App Store.

Process Google Play billing events

Google Play provides billing events through what they call a cloud pub/sub topic . These are essentially message queues that messages can be published on, as well as consumed from.

Because this is functionality specific to Google Play, you include this functionality in the GooglePlayPurchaseHandler .

Start by opening up lib/google_play_purchase_handler.dart , and adding the PubsubApi import:

lib/google_play_purchase_handler.dart

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

Then, pass the PubsubApi to the GooglePlayPurchaseHandler , and modify the class constructor to create a Timer as follows:

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

The Timer is configured to call the _pullMessageFromPubSub method every ten seconds. You can adjust the Duration to your own preference.

Then, create the _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,
    );
  }

The code you just added communicates with the Pub/Sub Topic from Google Cloud every ten seconds and asks for new messages. Then, processes each message in the _processMessage method.

This method decodes the incoming messages and obtains the updated information about each purchase, both subscriptions and non-subscriptions, calling the existing handleSubscription or handleNonSubscription if necessary.

Each message needs to be acknowledged with the _askMessage method.

Next, add the required dependencies to the server.dart file. Add the PubsubApi.cloudPlatformScope to the credentials configuration:

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
      ]);

Then, create the PubsubApi instance:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

And finally, pass it to the GooglePlayPurchaseHandler constructor:

bin/server.dart

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

Google Play setup

You've written the code to consume billing events from the pub/sub topic, but you haven't created the pub/sub topic, nor are you publishing any billing events. It's time to set this up.

First, create a pub/sub topic:

  1. Set the value of googleCloudProjectId in constants.dart to ID of your Google Cloud Project.
  2. Visit the Cloud Pub/Sub page on the Google Cloud Console.
  3. Ensure that you're on your Firebase project, and click + Create Topic . d5ebf6897a0a8bf5.png
  4. Give the new topic a name, identical to the value set for googlePlayPubsubBillingTopic in constants.dart . In this case, name it play_billing . If you choose something else, make sure to update constants.dart . Create the topic. 20d690fc543c4212.png
  5. In the list of your pub/sub topics, click the three vertical dots for the topic you just created, and click View permissions . ea03308190609fb.png
  6. In the sidebar on the right, choose Add principal .
  7. Here, add google-play-developer-notifications@system.gserviceaccount.com , and grant it the role of Pub/Sub Publisher . 55631ec0549215bc.png
  8. Save the permission changes.
  9. Copy the Topic name of the topic you've just created.
  10. Open the Play Console again, and choose your app from the All Apps list.
  11. Scroll down and go to Monetize > Monetization Setup .
  12. Fill in the full topic and save your changes. 7e5e875dc6ce5d54.png

All Google Play billing events will now be published on the topic.

Process App Store billing events

Next, do the same for the App Store billing events. There are two effective ways to implement handling updates in purchases for the App Store. One is by implementing a webhook that you provide to Apple and they use to communicate with your server. The second way, which is the one you will find in this codelab, is by connecting to the App Store Server API and obtaining the subscription information manually.

The reason why this codelab focuses on the second solution is because you would have to expose your server to the Internet in order to implement the webhook.

In a production environment, ideally you would like to have both. The webhook to obtain events from the App Store, and the Server API in case you missed an event or need to double check a subscription status.

Start by opening up lib/app_store_purchase_handler.dart , and adding the AppStoreServerAPI dependency:

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

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

Modify the constructor to add a timer that will call to the _pullStatus method. This timer will be calling the _pullStatus method every 10 seconds. You can adjust this timer duration to your needs.

lib/app_store_purchase_handler.dart

  AppStorePurchaseHandler(this.iapRepository, this.appStoreServerAPI) {
    // Poll Subscription status every 10 seconds.
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullStatus();
    });
  }

Then, create the _pullStatus method as follows:

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

This method works as follow:

  1. Obtains the list of active subscriptions from Firestore using the IapRepository.
  2. For each order, it requests the subscription status to the App Store Server API.
  3. Obtains the last transaction for that subscription purchase.
  4. Checks the expiration date.
  5. Updates the subscription status on Firestore, if it is expired it will be marked as such.

Finally, add all the necessary code to configure the App Store Server API access:

bin/server.dart

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


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

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

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

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

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

App Store setup

Next, set up the App Store:

  1. Login to App Store Connect , and select Users and Access .
  2. Go to Integrations > Keys > In-App Purchase .
  3. Tap on the "plus" icon to add a new one.
  4. Give it a name, such as "Codelab key".
  5. Download the p8 file containing the key.
  6. Copy it to the assets folder, with the name SubscriptionKey.p8 .
  7. Copy the key ID from the newly created key and set it to appStoreKeyId constant in the lib/constants.dart file.
  8. Copy the Issuer ID right at the top of the keys list, and set it to appStoreIssuerId constant in the lib/constants.dart file.

9540ea9ada3da151.png

Track purchases on the device

The most secure way to track your purchases is on the server side because the client is hard to secure, but you need to have some way to get the information back to the client so the app can act on the subscription status information. By storing the purchases in Firestore, you can sync the data to the client and keep it updated automatically.

You already included the IAPRepo in the app, which is the Firestore repository that contains all of the user's purchase data in List<PastPurchase> purchases . The repository also contains hasActiveSubscription, which is true when there is a purchase with productId storeKeySubscription with a status that is not expired. When the user isn't logged in, the list is empty.

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

All purchase logic is in the DashPurchases class and is where subscriptions should be applied or removed. So, add the iapRepo as a property in the class and assign the iapRepo in the constructor. Next, directly add a listener in the constructor, and remove the listener in the dispose() method. At first, the listener can just be an empty function. Because the IAPRepo is a ChangeNotifier and you call notifyListeners() every time the purchases in Firestore change, the purchasesUpdate() method is always called when the purchased products change.

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
  }

Next, supply the IAPRepo to the constructor in main.dart. You can get the repository by using context.read because it's already created in a Provider .

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
            context.read<IAPRepo>(),                         // Add this line
          ),
          lazy: false,
        ),

Next, write the code for the purchaseUpdate() function. In dash_counter.dart, the applyPaidMultiplier and removePaidMultiplier methods set the multiplier to 10 or 1, respectively, so you don't have to check whether the subscription is already applied. When the subscription status changes, you also update the status of the purchasable product so you can show in the purchase page that it's already active. Set the _beautifiedDashUpgrade property based on whether the upgrade is bought.

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

You have now ensured that the subscription and upgrade status is always current in the backend service and synchronized with the app. The app acts accordingly and applies the subscription and upgrade features to your Dash clicker game.

12. All done!

Congratulations!!! You have completed the codelab. You can find the completed code for this codelab in the android_studio_folder.png complete folder.

To learn more, try the other Flutter codelabs .