আপনার Flutter অ্যাপে অ্যাপ-মধ্যস্থ কেনাকাটা যোগ করা হচ্ছে

১. ভূমিকা

একটি ফ্লাটার অ্যাপে ইন-অ্যাপ পারচেজ যোগ করার জন্য অ্যাপ ও প্লে স্টোর সঠিকভাবে সেট আপ করা, ক্রয়টি যাচাই করা এবং সাবস্ক্রিপশন সুবিধার মতো প্রয়োজনীয় অনুমতি প্রদান করা প্রয়োজন।

এই কোডল্যাবে আপনি একটি অ্যাপে (যা আপনাকে দেওয়া হবে) তিন ধরনের ইন-অ্যাপ পারচেজ যোগ করবেন এবং ফায়ারবেস সহ একটি ডার্ট ব্যাকএন্ড ব্যবহার করে এই পারচেজগুলো ভেরিফাই করবেন। প্রদত্ত অ্যাপ, ড্যাশ ক্লিকার-এ একটি গেম রয়েছে যা ড্যাশ মাসকটকে মুদ্রা হিসেবে ব্যবহার করে। আপনাকে নিম্নলিখিত পারচেজ অপশনগুলো যোগ করতে হবে:

  1. একসাথে ২০০০ ড্যাশ কেনার জন্য পুনরাবৃত্তিযোগ্য বিকল্প।
  2. পুরানো ধাঁচের ড্যাশকে আধুনিক ধাঁচের ড্যাশে রূপান্তর করার জন্য এটি একটি এককালীন আপগ্রেড ক্রয়।
  3. এমন একটি সাবস্ক্রিপশন যা স্বয়ংক্রিয়ভাবে তৈরি হওয়া ক্লিকের সংখ্যা দ্বিগুণ করে দেয়।

প্রথম ক্রয়ের বিকল্পটি ব্যবহারকারীকে সরাসরি ২০০০ ড্যাশের সুবিধা দেয়। এগুলো ব্যবহারকারীর জন্য সরাসরি উপলব্ধ এবং বহুবার কেনা যায়। একে ব্যবহারযোগ্য বলা হয়, কারণ এটি সরাসরি খরচ হয়ে যায় এবং একাধিকবার ব্যবহার করা যায়।

দ্বিতীয় বিকল্পটি ড্যাশ-কে আরও সুন্দর একটি ড্যাশ-এ আপগ্রেড করে। এটি শুধুমাত্র একবারই কিনতে হয় এবং এটি চিরস্থায়ীভাবে উপলব্ধ থাকে। এই ধরনের ক্রয়কে নন-কনজিউমেবল বলা হয়, কারণ এটি অ্যাপের মাধ্যমে ব্যবহার করা যায় না, কিন্তু এটি চিরকালের জন্য বৈধ থাকে।

তৃতীয় এবং শেষ ক্রয়ের বিকল্পটি হলো সাবস্ক্রিপশন। সাবস্ক্রিপশন সক্রিয় থাকাকালীন ব্যবহারকারী আরও দ্রুত ড্যাশ পাবেন, কিন্তু যখন তিনি সাবস্ক্রিপশনের জন্য অর্থ প্রদান বন্ধ করে দেবেন, তখন এই সুবিধাও চলে যাবে।

ব্যাকএন্ড সার্ভিসটি (যা আপনাকেও সরবরাহ করা হয়েছে) একটি ডার্ট অ্যাপ হিসেবে চলে, কেনাকাটা সম্পন্ন হয়েছে কিনা তা যাচাই করে এবং ফায়ারস্টোর ব্যবহার করে সেগুলো সংরক্ষণ করে। প্রক্রিয়াটি সহজ করার জন্য ফায়ারস্টোর ব্যবহার করা হয়, কিন্তু আপনার প্রোডাকশন অ্যাপে আপনি যেকোনো ধরনের ব্যাকএন্ড সার্ভিস ব্যবহার করতে পারেন।

300123416ebc8dc1.png7145d0fffe6ea741.png646317a79be08214.png

আপনি যা তৈরি করবেন

  • আপনি ব্যবহারযোগ্য পণ্য ক্রয় এবং সাবস্ক্রিপশন সমর্থন করার জন্য অ্যাপটিকে সম্প্রসারিত করবেন।
  • এছাড়াও আপনি ক্রয়কৃত আইটেমগুলো যাচাই ও সংরক্ষণ করার জন্য একটি ডার্ট ব্যাকএন্ড অ্যাপ তৈরি করবেন।

আপনি যা শিখবেন

  • ক্রয়যোগ্য পণ্য দিয়ে অ্যাপ স্টোর এবং প্লে স্টোর কীভাবে কনফিগার করবেন
  • কেনাকাটা যাচাই করতে এবং ফায়ারস্টোরে সেগুলো সংরক্ষণ করতে স্টোরগুলোর সাথে কীভাবে যোগাযোগ করবেন।
  • আপনার অ্যাপে কেনাকাটা কীভাবে পরিচালনা করবেন

আপনার যা যা লাগবে

২. উন্নয়ন পরিবেশ সেট আপ করুন

এই কোডল্যাবটি শুরু করতে, কোডটি ডাউনলোড করুন এবং iOS-এর জন্য বান্ডেল আইডেন্টিফায়ার ও Android-এর জন্য প্যাকেজ নেম পরিবর্তন করুন।

কোডটি ডাউনলোড করুন

কমান্ড লাইন থেকে গিটহাব রিপোজিটরি ক্লোন করতে, নিম্নলিখিত কমান্ডটি ব্যবহার করুন:

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

অথবা, যদি আপনার GitHub-এর cli টুল ইনস্টল করা থাকে, তাহলে নিম্নলিখিত কমান্ডটি ব্যবহার করুন:

gh repo clone flutter/codelabs flutter-codelabs

স্যাম্পল কোডটি flutter-codelabs নামের একটি ডিরেক্টরিতে ক্লোন করা হয়েছে, যেখানে একাধিক কোডল্যাবের কোড রয়েছে। এই কোডল্যাবটির কোড flutter-codelabs/in_app_purchases ফোল্ডারে আছে।

flutter-codelabs/in_app_purchases অধীনে থাকা ডিরেক্টরি কাঠামোটিতে প্রতিটি নির্দিষ্ট ধাপের শেষে আপনার কোথায় থাকা উচিত, তার একটি ধারাবাহিক চিত্র রয়েছে। স্টার্টার কোডটি ধাপ ০-তে আছে, তাই নিম্নোক্তভাবে সেখানে যান:

cd flutter-codelabs/in_app_purchases/step_00

যদি আপনি সামনে এগিয়ে যেতে চান অথবা কোনো ধাপের পরে সেটি কেমন দেখাবে তা দেখতে চান, তাহলে আপনার কাঙ্ক্ষিত ধাপটির নামে থাকা ডিরেক্টরিতে দেখুন। শেষ ধাপের কোডটি ' complete ফোল্ডারের অধীনে রয়েছে।

স্টার্টার প্রজেক্টটি সেট আপ করুন।

step_00/app থেকে স্টার্টার প্রজেক্টটি আপনার পছন্দের IDE-তে খুলুন। আমরা স্ক্রিনশটগুলোর জন্য অ্যান্ড্রয়েড স্টুডিও ব্যবহার করেছি, তবে ভিজ্যুয়াল স্টুডিও কোডও একটি চমৎকার বিকল্প। যেকোনো এডিটরের ক্ষেত্রেই, নিশ্চিত করুন যে সর্বশেষ ডার্ট এবং ফ্লাটার প্লাগইনগুলো ইনস্টল করা আছে।

আপনার তৈরি করা অ্যাপগুলোকে অ্যাপ স্টোর এবং প্লে স্টোরের সাথে যোগাযোগ করতে হবে, যাতে জানা যায় কোন পণ্যগুলো কী দামে পাওয়া যাচ্ছে। প্রতিটি অ্যাপ একটি অনন্য আইডি দ্বারা চিহ্নিত করা হয়। iOS অ্যাপ স্টোরের জন্য এটিকে বান্ডেল আইডেন্টিফায়ার এবং অ্যান্ড্রয়েড প্লে স্টোরের জন্য এটিকে অ্যাপ্লিকেশন আইডি বলা হয়। এই আইডেন্টিফায়ারগুলো সাধারণত রিভার্স ডোমেইন নেম নোটেশন ব্যবহার করে তৈরি করা হয়। উদাহরণস্বরূপ, flutter.dev-এর জন্য একটি ইন-অ্যাপ পারচেজ অ্যাপ তৈরি করার সময় আপনি dev.flutter.inapppurchase ব্যবহার করবেন। আপনার অ্যাপের জন্য একটি আইডেন্টিফায়ার ভাবুন, যা আপনি এখন প্রজেক্ট সেটিংসে সেট করবেন।

প্রথমে, iOS-এর জন্য বান্ডেল আইডেন্টিফায়ার সেট আপ করুন। এটি করার জন্য, Xcode অ্যাপে Runner.xcworkspace ফাইলটি খুলুন।

a9fbac80a31e28e0.png

Xcode-এর ফোল্ডার কাঠামোতে, Runner প্রজেক্টটি সবার উপরে থাকে এবং এর নিচে Flutter , RunnerProducts টার্গেটগুলো থাকে। আপনার প্রজেক্ট সেটিংস সম্পাদনা করতে Runner-এর উপর ডাবল-ক্লিক করুন এবং Signing & Capabilities- এ ক্লিক করুন। আপনার টিম সেট করার জন্য Team ফিল্ডের অধীনে আপনার সদ্য বেছে নেওয়া বান্ডেল আইডেন্টিফায়ারটি প্রবেশ করান।

812f919d965c649a.jpeg

এখন আপনি Xcode বন্ধ করে Android Studio-তে ফিরে গিয়ে Android-এর কনফিগারেশন শেষ করতে পারেন। এটি করার জন্য android/app, অধীনে থাকা build.gradle.kts ফাইলটি খুলুন এবং আপনার applicationId (নিচের স্ক্রিনশটের ২৪ নম্বর লাইনে) পরিবর্তন করে iOS বান্ডেল আইডেন্টিফায়ারের মতো একই অ্যাপ্লিকেশন আইডি দিন। মনে রাখবেন যে iOS এবং Android স্টোরের আইডিগুলো একই হতে হবে এমন কোনো বাধ্যবাধকতা নেই, তবে এগুলো একই রাখলে ভুলের সম্ভাবনা কম থাকে এবং তাই এই কোডল্যাবে আমরা একই আইডেন্টিফায়ার ব্যবহার করব।

e320a49ff2068ac2.png

৩. প্লাগইনটি ইনস্টল করুন।

কোডল্যাবের এই অংশে আপনি in_app_purchase প্লাগইনটি ইনস্টল করবেন।

pubspec-এ নির্ভরতা যোগ করুন

আপনার প্রোজেক্টের ডিপেন্ডেন্সিতে in_app_purchase যোগ করে pubspec-এ in_app_purchase যুক্ত করুন:

$ 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 খুলুন এবং নিশ্চিত করুন যে এখন dependencies অধীনে in_app_purchase এবং dev_dependencies অধীনে in_app_purchase_platform_interface একটি এন্ট্রি হিসেবে তালিকাভুক্ত আছে।

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-এ ইন-অ্যাপ পারচেজ সেট আপ করতে এবং পরীক্ষা করতে, আপনাকে অ্যাপ স্টোরে একটি নতুন অ্যাপ তৈরি করতে হবে এবং সেখানে ক্রয়যোগ্য প্রোডাক্ট তৈরি করতে হবে। আপনাকে কোনো কিছু পাবলিশ করতে হবে না বা পর্যালোচনার জন্য অ্যাপটি অ্যাপলের কাছে পাঠাতে হবে না। এটি করার জন্য আপনার একটি ডেভেলপার অ্যাকাউন্ট প্রয়োজন। যদি আপনার অ্যাকাউন্ট না থাকে, তাহলে অ্যাপল ডেভেলপার প্রোগ্রামে নথিভুক্ত হন

ইন-অ্যাপ পারচেজ ব্যবহার করার জন্য, অ্যাপ স্টোর কানেক্ট-এ পেইড অ্যাপের জন্য আপনার একটি সক্রিয় চুক্তিও থাকতে হবে। https://appstoreconnect.apple.com/ -এ যান এবং Agreements, Tax, and Banking -এ ক্লিক করুন।

11db9fca823e7608.png

এখানে আপনি ফ্রি এবং পেইড অ্যাপের জন্য চুক্তিপত্র দেখতে পাবেন। ফ্রি অ্যাপের স্ট্যাটাস 'অ্যাক্টিভ' এবং পেইড অ্যাপের স্ট্যাটাস 'নিউ' হওয়া উচিত। নিশ্চিত করুন যে আপনি শর্তাবলী দেখেছেন, সেগুলো গ্রহণ করেছেন এবং সমস্ত প্রয়োজনীয় তথ্য পূরণ করেছেন।

74c73197472c9aec.png

যখন সবকিছু সঠিকভাবে সেট করা হবে, তখন পেইড অ্যাপগুলোর স্ট্যাটাস সক্রিয় হয়ে যাবে। এটি খুবই গুরুত্বপূর্ণ, কারণ একটি সক্রিয় চুক্তি ছাড়া আপনি ইন-অ্যাপ পারচেজগুলো চেষ্টা করে দেখতে পারবেন না।

4a100bbb8cafdbbf.jpeg

অ্যাপ আইডি নিবন্ধন করুন

অ্যাপল ডেভেলপার পোর্টালে একটি নতুন আইডেন্টিফায়ার তৈরি করুন। developer.apple.com/account/resources/identifiers/list- এ যান এবং Identifiers হেডারের পাশে থাকা 'প্লাস' আইকনে ক্লিক করুন।

55d7e592d9a3fc7b.png

অ্যাপ আইডি নির্বাচন করুন

13f125598b72ca77.png

অ্যাপ বেছে নিন

41ac4c13404e2526.png

একটি বিবরণ দিন এবং বান্ডেল আইডিটি XCode-এ পূর্বে সেট করা মানের সাথে মিলিয়ে সেট করুন।

9d2c940ad80deeef.png

নতুন অ্যাপ আইডি কীভাবে তৈরি করতে হয় সে সম্পর্কে আরও নির্দেশনার জন্য, ডেভেলপার অ্যাকাউন্ট হেল্প দেখুন।

একটি নতুন অ্যাপ তৈরি করা

অ্যাপ স্টোরে আপনার অনন্য বান্ডেল আইডেন্টিফায়ার দিয়ে একটি নতুন অ্যাপ তৈরি করুন।

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

কীভাবে একটি নতুন অ্যাপ তৈরি করতে হয় এবং চুক্তিগুলি পরিচালনা করতে হয় সে সম্পর্কে আরও নির্দেশনার জন্য, অ্যাপ স্টোর কানেক্ট হেল্প দেখুন।

ইন-অ্যাপ পারচেজ পরীক্ষা করার জন্য, আপনার একটি স্যান্ডবক্স টেস্ট ইউজার প্রয়োজন। এই টেস্ট ইউজারটি আইটিউনস-এর সাথে সংযুক্ত থাকা উচিত নয়—এটি শুধুমাত্র ইন-অ্যাপ পারচেজ পরীক্ষা করার জন্য ব্যবহৃত হয়। আপনি এমন কোনো ইমেল অ্যাড্রেস ব্যবহার করতে পারবেন না যা ইতিমধ্যেই একটি অ্যাপল অ্যাকাউন্টের জন্য ব্যবহৃত হচ্ছে। ইউজার্স অ্যান্ড অ্যাক্সেস (Users and Access) -এ গিয়ে স্যান্ডবক্স (Sandbox) অপশনে একটি নতুন স্যান্ডবক্স অ্যাকাউন্ট তৈরি করতে বা বিদ্যমান স্যান্ডবক্স অ্যাপল আইডিগুলো পরিচালনা করতে পারেন।

2ba0f599bcac9b36.png

এখন আপনি আপনার আইফোনে সেটিংস > ডেভেলপার > স্যান্ডবক্স অ্যাপল অ্যাকাউন্ট-এ গিয়ে আপনার স্যান্ডবক্স ব্যবহারকারী সেট আপ করতে পারেন।

74a545210b282ad8.pngeaa67752f2350f74.png

আপনার ইন-অ্যাপ কেনাকাটা কনফিগার করা

এখন আপনি ক্রয়যোগ্য তিনটি আইটেম কনফিগার করবেন:

  • dash_consumable_2k : একটি ব্যবহারযোগ্য পণ্য যা একাধিকবার কেনা যায় এবং প্রতিবার কেনার জন্য ব্যবহারকারী ২০০০ ড্যাশ (অ্যাপের অভ্যন্তরীণ মুদ্রা) পান।
  • dash_upgrade_3d : একটি অ-ব্যবহারযোগ্য "আপগ্রেড" যা শুধুমাত্র একবারই কেনা যায় এবং এটি ব্যবহারকারীকে ক্লিক করার জন্য বাহ্যিকভাবে ভিন্ন একটি ড্যাশ প্রদান করে।
  • dash_subscription_doubler : এমন একটি সাবস্ক্রিপশন যা ব্যবহারকারীকে সাবস্ক্রিপশনের সময়কাল জুড়ে প্রতি ক্লিকে দ্বিগুণ সংখ্যক ড্যাশ ব্যবহারের সুযোগ দেয়।

a118161fac83815a.png

ইন-অ্যাপ পারচেজ- এ যান।

নির্দিষ্ট আইডিগুলো ব্যবহার করে আপনার ইন-অ্যাপ কেনাকাটা তৈরি করুন:

  1. dash_consumable_2k একটি Consumable হিসেবে সেট আপ করুন। Product ID হিসেবে dash_consumable_2k ব্যবহার করুন। রেফারেন্স নামটি শুধুমাত্র App Store Connect-এ ব্যবহৃত হয়, এটিকে শুধু dash consumable 2k তে সেট করুন। 1f8527fc03902099.png প্রাপ্যতা সেট আপ করুন। পণ্যটি স্যান্ডবক্স ব্যবহারকারীর দেশে অবশ্যই উপলব্ধ থাকতে হবে। bd6b2ce2d9314e6e.png মূল্য যোগ করুন এবং দামটি $1.99 বা অন্য মুদ্রায় এর সমতুল্য পরিমাণ নির্ধারণ করুন। 926b03544ae044c4.png ক্রয়ের জন্য আপনার স্থানীয়করণ যোগ করুন। ক্রয়টির নাম দিন ‘Spring is in the air with 2000 dashes fly out এবং বিবরণ হিসেবে লিখুন Spring is in the aire26dd4f966dcfece.png একটি রিভিউ স্ক্রিনশট যোগ করুন। প্রোডাক্টটি রিভিউর জন্য পাঠানো না হলে এর বিষয়বস্তু গুরুত্বপূর্ণ নয়, কিন্তু প্রোডাক্টটিকে "Ready to Submit" অবস্থায় থাকার জন্য এটি আবশ্যক, যা অ্যাপটি অ্যাপ স্টোর থেকে প্রোডাক্ট সংগ্রহ করার সময় প্রয়োজন হয়। 25171bfd6f3a033a.png
  2. dash_upgrade_3d একটি Non-consumable হিসেবে সেট আপ করুন। প্রোডাক্ট আইডি হিসেবে dash_upgrade_3d ব্যবহার করুন। রেফারেন্স নামটি dash upgrade 3d হিসেবে সেট করুন। পারচেজটির নাম দিন 3D Dash এবং ডেসক্রিপশন হিসেবে লিখুন Brings your dash back to the future । মূল্য $0.99 নির্ধারণ করুন। dash_consumable_2k প্রোডাক্টের মতোই অ্যাভেইলেবিলিটি কনফিগার করুন এবং রিভিউ স্ক্রিনশট আপলোড করুন। 83878759f32a7d4a.png
  3. dash_subscription_doubler একটি স্বয়ংক্রিয়ভাবে নবায়নযোগ্য সাবস্ক্রিপশন হিসেবে সেট আপ করুন। সাবস্ক্রিপশনের প্রক্রিয়াটি কিছুটা ভিন্ন। প্রথমে, আপনাকে একটি সাবস্ক্রিপশন গ্রুপ তৈরি করতে হবে। যখন একাধিক সাবস্ক্রিপশন একই গ্রুপের অংশ হয়, তখন একজন ব্যবহারকারী একই সময়ে এগুলোর মধ্যে কেবল একটিতে সাবস্ক্রাইব করতে পারেন, কিন্তু এই সাবস্ক্রিপশনগুলোর মধ্যে আপগ্রেড বা ডাউনগ্রেড করতে পারেন। এই গ্রুপটির নাম দিন subscriptions393a44b09f3cd8bf.png এবং সাবস্ক্রিপশন গ্রুপের জন্য স্থানীয়করণ যোগ করুন। 595aa910776349bd.png এরপর আপনি সাবস্ক্রিপশনটি তৈরি করবেন। রেফারেন্স নেম হিসেবে dash subscription doubler এবং প্রোডাক্ট আইডি হিসেবে ` dash_subscription_doubler সেট করুন। 7bfff7bbe11c8eec.png এরপর, ১ সপ্তাহের সাবস্ক্রিপশন সময়কাল এবং স্থানীয়করণ নির্বাচন করুন। এই সাবস্ক্রিপশনটির নাম দিন Jet Engine এবং বিবরণ দিন Doubles your clicks ’। মূল্য $0.49 নির্ধারণ করুন। dash_consumable_2k প্রোডাক্টটির মতোই প্রাপ্যতা কনফিগার করুন এবং রিভিউ স্ক্রিনশট আপলোড করুন। 44d18e02b926a334.png

এখন আপনি তালিকাগুলিতে পণ্যগুলি দেখতে পাবেন:

17f242b5c1426b79.pngd71da951f595054a.png

৫. প্লে স্টোর সেট আপ করুন

অ্যাপ স্টোরের মতোই, প্লে স্টোরের জন্যও আপনার একটি ডেভেলপার অ্যাকাউন্ট লাগবে। যদি আপনার এখনও কোনো অ্যাকাউন্ট না থাকে, তাহলে একটি রেজিস্টার করুন

একটি নতুন অ্যাপ তৈরি করুন

গুগল প্লে কনসোলে একটি নতুন অ্যাপ তৈরি করুন:

  1. প্লে কনসোলটি খুলুন।
  2. সকল অ্যাপ নির্বাচন করুন > অ্যাপ তৈরি করুন।
  3. আপনার অ্যাপের জন্য একটি ডিফল্ট ভাষা নির্বাচন করুন এবং একটি শিরোনাম যোগ করুন। গুগল প্লে-তে আপনার অ্যাপটি যেভাবে দেখাতে চান, সেই নামটি টাইপ করুন। আপনি পরে নামটি পরিবর্তন করতে পারবেন।
  4. আপনার অ্যাপ্লিকেশনটি যে একটি গেম, তা উল্লেখ করুন। আপনি এটি পরে পরিবর্তন করতে পারবেন।
  5. আপনার অ্যাপ্লিকেশনটি বিনামূল্যে নাকি সशुल्क, তা উল্লেখ করুন।
  6. বিষয়বস্তু নির্দেশিকা এবং মার্কিন রপ্তানি আইন সংক্রান্ত ঘোষণাপত্রগুলো সম্পূর্ণ করুন।
  7. অ্যাপ তৈরি করুন নির্বাচন করুন।

আপনার অ্যাপটি তৈরি হয়ে গেলে, ড্যাশবোর্ডে যান এবং 'আপনার অ্যাপ সেট আপ করুন' বিভাগের সমস্ত কাজ সম্পন্ন করুন। এখানে, আপনাকে আপনার অ্যাপ সম্পর্কে কিছু তথ্য দিতে হবে, যেমন কন্টেন্ট রেটিং এবং স্ক্রিনশট। 13845badcf9bc1db.png

আবেদনপত্রে স্বাক্ষর করুন

ইন-অ্যাপ পারচেজ পরীক্ষা করতে হলে, গুগল প্লে-তে আপনার অন্তত একটি বিল্ড আপলোড করা থাকতে হবে।

এর জন্য, আপনার রিলিজ বিল্ডটি ডিবাগ কীগুলো ছাড়া অন্য কিছু দিয়ে স্বাক্ষরিত হতে হবে।

একটি কীস্টোর তৈরি করুন

আপনার যদি আগে থেকে কোনো কীস্টোর থাকে, তাহলে পরবর্তী ধাপে যান। অন্যথায়, কমান্ড লাইনে নিম্নলিখিত কমান্ডটি চালিয়ে একটি তৈরি করুন।

Mac/Linux-এ, নিম্নলিখিত কমান্ডটি ব্যবহার করুন:

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 নামে একটি ফাইল তৈরি করুন, যাতে আপনার কীস্টোরের একটি রেফারেন্স থাকবে:

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 ফাইলটি সম্পাদনা করুন।

আপনার প্রোপার্টিজ ফাইল থেকে কীস্টোরের তথ্য 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")
        }
    }

আপনার মডিউলের build.gradle.kts ফাইলের signingConfigs ব্লকটি সাইনিং কনফিগারেশন তথ্য দিয়ে কনফিগার করুন:

   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-'Sign your app' দেখুন।

আপনার প্রথম বিল্ড আপলোড করুন

আপনার অ্যাপটি সাইন করার জন্য কনফিগার করার পরে, নিম্নলিখিত কমান্ডটি চালিয়ে আপনি আপনার অ্যাপ্লিকেশনটি বিল্ড করতে পারবেন:

flutter build appbundle

এই কমান্ডটি ডিফল্টরূপে একটি রিলিজ বিল্ড তৈরি করে এবং এর আউটপুট <your app dir>/build/app/outputs/bundle/release/ -এ পাওয়া যাবে।

গুগল প্লে কনসোলের ড্যাশবোর্ড থেকে, টেস্ট অ্যান্ড রিলিজ > টেস্টিং > ক্লোজড টেস্টিং- এ যান এবং একটি নতুন, ক্লোজড টেস্টিং রিলিজ তৈরি করুন।

এরপরে, বিল্ড কমান্ডের মাধ্যমে তৈরি হওয়া app-release.aab অ্যাপ বান্ডেলটি আপলোড করুন।

সেভ-এ ক্লিক করুন এবং তারপরে রিভিউ রিলিজ-এ ক্লিক করুন।

অবশেষে, ক্লোজড টেস্টিং রিলিজটি সক্রিয় করতে 'স্টার্ট রোলআউট টু ক্লোজড টেস্টিং'-এ ক্লিক করুন।

পরীক্ষামূলক ব্যবহারকারী সেট আপ করুন

ইন-অ্যাপ পারচেজ পরীক্ষা করতে হলে, আপনার পরীক্ষকদের গুগল অ্যাকাউন্টগুলো গুগল প্লে কনসোলের দুটি স্থানে যুক্ত করতে হবে:

  1. নির্দিষ্ট টেস্ট ট্র্যাকে (অভ্যন্তরীণ পরীক্ষা)
  2. লাইসেন্স পরীক্ষক হিসেবে

প্রথমে, পরীক্ষককে অভ্যন্তরীণ টেস্টিং ট্র্যাকে যুক্ত করার মাধ্যমে শুরু করুন। Test and release > Testing > Internal testing- এ ফিরে যান এবং Testers ট্যাবে ক্লিক করুন।

a0d0394e85128f84.png

'Create email list'-এ ক্লিক করে একটি নতুন ইমেল তালিকা তৈরি করুন। তালিকাটির একটি নাম দিন এবং সেই Google অ্যাকাউন্টগুলির ইমেল ঠিকানা যোগ করুন যেগুলির ইন-অ্যাপ কেনাকাটা পরীক্ষা করার জন্য অ্যাক্সেস প্রয়োজন।

এরপর, তালিকার জন্য চেকবক্সটি নির্বাচন করুন এবং 'পরিবর্তনগুলি সংরক্ষণ করুন'- এ ক্লিক করুন।

তারপর, লাইসেন্স পরীক্ষকদের যুক্ত করুন:

  1. গুগল প্লে কনসোলের সমস্ত অ্যাপ ভিউতে ফিরে যান।
  2. সেটিংস > লাইসেন্স টেস্টিং- এ যান।
  3. যেসব পরীক্ষকদের ইন-অ্যাপ কেনাকাটা পরীক্ষা করার প্রয়োজন হবে, তাদের একই ইমেল ঠিকানাগুলো যোগ করুন।
  4. লাইসেন্সের প্রতিক্রিয়া RESPOND_NORMALLY তে সেট করুন।
  5. পরিবর্তনগুলি সংরক্ষণ করুন-এ ক্লিক করুন।

a1a0f9d3e55ea8da.png

আপনার ইন-অ্যাপ কেনাকাটা কনফিগার করা

এখন আপনি অ্যাপের মধ্যে থেকে ক্রয়যোগ্য আইটেমগুলো কনফিগার করবেন।

অ্যাপ স্টোরের মতোই, আপনাকে তিনটি ভিন্ন ক্রয় নির্ধারণ করতে হবে:

  • dash_consumable_2k : একটি ব্যবহারযোগ্য পণ্য যা একাধিকবার কেনা যায় এবং প্রতিবার কেনার জন্য ব্যবহারকারী ২০০০ ড্যাশ (অ্যাপের অভ্যন্তরীণ মুদ্রা) পান।
  • dash_upgrade_3d : একটি অ-ব্যবহারযোগ্য "আপগ্রেড" যা শুধুমাত্র একবারই কেনা যায় এবং এটি ব্যবহারকারীকে ক্লিক করার জন্য বাহ্যিকভাবে ভিন্ন একটি ড্যাশ প্রদান করে।
  • dash_subscription_doubler : এমন একটি সাবস্ক্রিপশন যা ব্যবহারকারীকে সাবস্ক্রিপশনের সময়কাল জুড়ে প্রতি ক্লিকে দ্বিগুণ সংখ্যক ড্যাশ ব্যবহারের সুযোগ দেয়।

প্রথমে, ব্যবহারযোগ্য এবং ব্যবহার-অযোগ্য জিনিসগুলো যোগ করুন।

  1. গুগল প্লে কনসোলে যান এবং আপনার অ্যাপ্লিকেশনটি নির্বাচন করুন।
  2. Monetize > Products > In-app products- এ যান।
  3. পণ্য তৈরি করতে ক্লিক করুন c8d66e32f57dee21.png
  4. আপনার পণ্যের জন্য প্রয়োজনীয় সমস্ত তথ্য প্রবেশ করান। নিশ্চিত করুন যে প্রোডাক্ট আইডিটি আপনি যে আইডিটি ব্যবহার করতে চান তার সাথে হুবহু মিলে যায়।
  5. সংরক্ষণ করুন-এ ক্লিক করুন।
  6. সক্রিয় করুন- এ ক্লিক করুন।
  7. অব্যবহারযোগ্য 'আপগ্রেড' ক্রয়ের ক্ষেত্রেও প্রক্রিয়াটি পুনরাবৃত্তি করুন।

এরপর, সাবস্ক্রিপশনটি যোগ করুন:

  1. গুগল প্লে কনসোলে যান এবং আপনার অ্যাপ্লিকেশনটি নির্বাচন করুন।
  2. Monetize > Products > Subscriptions- এ যান।
  3. সাবস্ক্রিপশন তৈরি করতে ক্লিক করুন 32a6a9eefdb71dd0.png
  4. আপনার সাবস্ক্রিপশনের জন্য প্রয়োজনীয় সমস্ত তথ্য প্রবেশ করান। নিশ্চিত করুন যে প্রোডাক্ট আইডিটি আপনার ব্যবহার করতে চাওয়া আইডির সাথে হুবহু মিলে যায়।
  5. সংরক্ষণ করুন ক্লিক করুন

আপনার কেনাকাটাগুলো এখন প্লে কনসোলে সেট আপ করা হয়ে গেছে।

৬. ফায়ারবেস সেট আপ করুন

এই কোডল্যাবে, আপনি ব্যবহারকারীদের কেনাকাটা যাচাই ও ট্র্যাক করার জন্য একটি ব্যাকএন্ড সার্ভিস ব্যবহার করবেন।

ব্যাকএন্ড পরিষেবা ব্যবহারের বেশ কিছু সুবিধা রয়েছে:

  • আপনি নিরাপদে লেনদেন যাচাই করতে পারেন।
  • আপনি অ্যাপ স্টোর থেকে বিলিং ইভেন্টগুলিতে প্রতিক্রিয়া জানাতে পারেন।
  • আপনি একটি ডেটাবেসে ক্রয়ের হিসাব রাখতে পারেন।
  • ব্যবহারকারীরা তাদের সিস্টেমের ঘড়ি পিছিয়ে দিয়ে আপনার অ্যাপকে প্রিমিয়াম ফিচার প্রদান করতে ধোঁকা দিতে পারবে না।

যদিও ব্যাকএন্ড সার্ভিস সেট আপ করার অনেক উপায় আছে, আপনি গুগলের নিজস্ব ফায়ারবেস ব্যবহার করে ক্লাউড ফাংশন ও ফায়ারস্টোরের মাধ্যমে এটি করবেন।

এই কোডল্যাবের আওতার বাইরে ব্যাকএন্ড লেখা অন্তর্ভুক্ত করা হয়েছে, তাই আপনাকে কাজ শুরু করতে সাহায্য করার জন্য স্টার্টার কোডে আগে থেকেই একটি ফায়ারবেস প্রজেক্ট যোগ করা আছে, যা সাধারণ কেনাকাটার কাজগুলো পরিচালনা করে।

স্টার্টার অ্যাপটির সাথে ফায়ারবেস প্লাগইনগুলোও অন্তর্ভুক্ত রয়েছে।

এখন আপনাকে যা করতে হবে তা হলো, নিজের একটি ফায়ারবেস প্রজেক্ট তৈরি করা, ফায়ারবেসের জন্য অ্যাপ ও ব্যাকএন্ড উভয়ই কনফিগার করা এবং সবশেষে ব্যাকএন্ডটি ডিপ্লয় করা।

একটি ফায়ারবেস প্রজেক্ট তৈরি করুন

ফায়ারবেস কনসোলে যান এবং একটি নতুন ফায়ারবেস প্রজেক্ট তৈরি করুন। এই উদাহরণের জন্য, প্রজেক্টটির নাম দিন ড্যাশ ক্লিকার।

ব্যাকএন্ড অ্যাপে আপনি কেনাকাটাগুলোকে একজন নির্দিষ্ট ব্যবহারকারীর সাথে যুক্ত করেন, তাই আপনার অথেনটিকেশন প্রয়োজন। এর জন্য, গুগল সাইন-ইন সহ ফায়ারবেসের অথেনটিকেশন মডিউল ব্যবহার করুন।

  1. Firebase ড্যাশবোর্ড থেকে Authentication- এ যান এবং প্রয়োজন হলে এটি সক্রিয় করুন।
  2. সাইন-ইন পদ্ধতি ট্যাবে যান এবং গুগল সাইন-ইন প্রদানকারীকে সক্রিয় করুন।

fe2e0933d6810888.png

যেহেতু আপনি Firebase-এর Firestore ডেটাবেসও ব্যবহার করবেন, তাই এটিও সক্রিয় করুন।

d02d641821c71e2c.png

ক্লাউড ফায়ারস্টোর নিয়মগুলো এইভাবে সেট করুন:

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

ফ্লাটারের জন্য ফায়ারবেস সেট আপ করুন

ফ্লাটার অ্যাপে ফায়ারবেস ইনস্টল করার জন্য ফ্লাটারফায়ার সিএলআই (FlutterFire CLI) ব্যবহার করার পরামর্শ দেওয়া হয়। সেটআপ পৃষ্ঠায় ব্যাখ্যা করা নির্দেশাবলী অনুসরণ করুন।

`flutterfire configure` চালানোর সময়, পূর্ববর্তী ধাপে আপনার তৈরি করা প্রজেক্টটি নির্বাচন করুন।

$ flutterfire configure

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

এরপর, দুটি প্ল্যাটফর্ম নির্বাচন করে iOS এবং Android সক্রিয় করুন।

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

firebase_options.dart ওভাররাইড করার বিষয়ে জিজ্ঞাসা করা হলে, 'হ্যাঁ' নির্বাচন করুন।

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

অ্যান্ড্রয়েডের জন্য ফায়ারবেস সেট আপ করুন: পরবর্তী পদক্ষেপ

Firebase ড্যাশবোর্ড থেকে, Project Overview-তে যান, Settings বেছে নিন এবং General ট্যাবটি নির্বাচন করুন।

নিচে স্ক্রল করে Your apps- এ যান এবং dashclicker (android) অ্যাপটি নির্বাচন করুন।

b22d46a759c0c834.png

ডিবাগ মোডে গুগল সাইন-ইন চালু করতে হলে, আপনাকে আপনার ডিবাগ সার্টিফিকেটের SHA-1 হ্যাশ ফিঙ্গারপ্রিন্ট প্রদান করতে হবে।

আপনার ডিবাগ সাইনিং সার্টিফিকেট হ্যাশটি নিন

আপনার ফ্লাটার অ্যাপ প্রজেক্টের রুটে, android/ ফোল্ডারে যান এবং একটি সাইনিং রিপোর্ট তৈরি করুন।

cd android
./gradlew :app:signingReport

আপনার সামনে সাইনিং কী-গুলির একটি বড় তালিকা দেখানো হবে। যেহেতু আপনি ডিবাগ সার্টিফিকেটের হ্যাশ খুঁজছেন, তাই সেই সার্টিফিকেটটি খুঁজুন যার Variant এবং Config প্রপার্টি debug এ সেট করা আছে। কীস্টোরটি সম্ভবত আপনার হোম ফোল্ডারে .android/debug.keystore অধীনে থাকবে।

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

SHA-1 হ্যাশটি কপি করুন এবং অ্যাপ জমা দেওয়ার মডাল ডায়ালগের শেষ ফিল্ডটি পূরণ করুন।

সবশেষে, সাইনিং কনফিগারেশন অন্তর্ভুক্ত করার জন্য অ্যাপটি আপডেট করতে আবার 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-এর জন্য Firebase সেট আপ করুন: পরবর্তী পদক্ষেপসমূহ

Xcode দিয়ে ios/Runner.xcworkspace টি খুলুন। অথবা আপনার পছন্দের IDE দিয়ে খুলুন।

VSCode-এ ios/ ফোল্ডারটির উপর রাইট-ক্লিক করুন এবং তারপর open in xcode

অ্যান্ড্রয়েড স্টুডিওতে ios/ ফোল্ডারের উপর রাইট-ক্লিক করে flutter ক্লিক করুন এবং তারপরে open iOS module in Xcode

iOS-এ গুগল সাইন-ইন চালু করার জন্য, আপনার বিল্ড plist ফাইলগুলিতে CFBundleURLTypes কনফিগারেশন অপশনটি যোগ করুন। (আরও তথ্যের জন্য google_sign_in প্যাকেজের ডকুমেন্টেশন দেখুন।) এক্ষেত্রে, ফাইলটি হলো ios/Runner/Info.plist

কী-ভ্যালু পেয়ারটি ইতিমধ্যেই যোগ করা হয়েছে, কিন্তু তাদের ভ্যালুগুলো অবশ্যই প্রতিস্থাপন করতে হবে:

  1. GoogleService-Info.plist ফাইল থেকে REVERSED_CLIENT_ID এর মানটি নিন, তবে এর চারপাশের <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>

আপনার ফায়ারবেস সেটআপ এখন সম্পন্ন হয়েছে।

৭. ক্রয়ের হালনাগাদ তথ্য শুনুন

কোডল্যাবের এই অংশে আপনি পণ্য ক্রয়ের জন্য অ্যাপটি প্রস্তুত করবেন। এই প্রক্রিয়ার মধ্যে অ্যাপটি চালু হওয়ার পর ক্রয়ের আপডেট এবং ত্রুটিগুলো পর্যবেক্ষণ করা অন্তর্ভুক্ত।

ক্রয়ের আপডেট শুনুন

main.dart,MyHomePage উইজেটটি খুঁজুন, যেটিতে একটি Scaffold আছে এবং সেই Scaffold-এর BottomNavigationBar টিতে দুটি পেজ রয়েছে। এই পেজটি DashCounter , DashUpgrades, এবং DashPurchases জন্য তিনটি Provider ও তৈরি করে। DashCounter ড্যাশের বর্তমান সংখ্যা ট্র্যাক করে এবং স্বয়ংক্রিয়ভাবে তা বৃদ্ধি করে। DashUpgrades সেই আপগ্রেডগুলো পরিচালনা করে যা আপনি ড্যাশ দিয়ে কিনতে পারেন। এই কোডল্যাবটি DashPurchases উপর আলোকপাত করে।

ডিফল্টরূপে, একটি প্রোভাইডারের অবজেক্ট প্রথমবার অনুরোধ করার সময় সংজ্ঞায়িত করা হয়। এই অবজেক্টটি অ্যাপ শুরু হওয়ার সাথে সাথে সরাসরি ক্রয়ের আপডেট শোনে, তাই lazy: false ব্যবহার করে এই অবজেক্টে লেজি লোডিং নিষ্ক্রিয় করুন।

lib/main.dart

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

আপনার InAppPurchaseConnection এর একটি ইনস্ট্যান্সও প্রয়োজন হবে। তবে, অ্যাপটিকে টেস্টযোগ্য রাখতে কানেকশনটি মক করার জন্য আপনার কোনো একটি উপায় দরকার। এটি করার জন্য, একটি ইনস্ট্যান্স মেথড তৈরি করুন যা টেস্টের সময় ওভাররাইড করা যাবে, এবং সেটিকে main.dart এ যুক্ত করুন।

lib/main.dart

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

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

পরীক্ষাটি নিম্নরূপভাবে আপডেট করুন:

test/widget_test.dart

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

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

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

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

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

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

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

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

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

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

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

lib/logic/dash_purchases.dart ফাইলে, DashPurchasesChangeNotifier এর কোডে যান। এই পর্যায়ে, সেখানে শুধুমাত্র একটি DashCounter আছে যা আপনি আপনার কেনা ড্যাশগুলোতে যোগ করতে পারবেন।

একটি স্ট্রিম সাবস্ক্রিপশন প্রপার্টি, _subscription ( StreamSubscription<List<PurchaseDetails>> _subscription; টাইপের), IAPConnection.instance, এবং ইম্পোর্টগুলো যোগ করুন। এর ফলে কোডটি দেখতে নিম্নলিখিতের মতো হবে:

lib/logic/dash_purchases.dart

import 'dart:async';

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

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

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

  bool get beautifiedDash => false;

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

  DashPurchases(this.counter);

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

_subscription এর সাথে late কীওয়ার্ডটি যুক্ত করা হয়, কারণ _subscription কনস্ট্রাক্টরে ইনিশিয়ালাইজ করা হয়। এই প্রজেক্টটি ডিফল্টভাবে নন-নালএবল (NNBD) হিসেবে সেট আপ করা হয়েছে, যার অর্থ হলো যে প্রোপার্টিগুলো নালএবল হিসেবে ডিক্লেয়ার করা হয়নি, সেগুলোর ভ্যালু অবশ্যই একটি নন-নাল হতে হবে। 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 একটি নকল পণ্য প্রদর্শন করে। আসল বিষয়বস্তু দেখানোর জন্য, purchasable_product.dart এ থাকা PurchasableProduct ক্লাসটিকে নিম্নলিখিত কোড দিয়ে প্রতিস্থাপন করে এটিকে আপডেট করুন:

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus { purchasable, purchased, pending }

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

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

dash_purchases.dart, ডামি পারচেজগুলো সরিয়ে দিন এবং সেগুলোর জায়গায় একটি খালি লিস্ট, List<PurchasableProduct> products = []; যোগ করুন।

উপলব্ধ ক্রয় লোড করুন

ব্যবহারকারীকে কেনাকাটার সুযোগ দিতে, স্টোর থেকে কেনাকাটার তথ্য লোড করুন। প্রথমে, স্টোরটি উপলব্ধ আছে কিনা তা পরীক্ষা করুন। যখন স্টোরটি উপলব্ধ থাকে না, তখন storeState notAvailable এ সেট করলে ব্যবহারকারীকে একটি ত্রুটির বার্তা দেখানো হয়।

lib/logic/dash_purchases.dart

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

স্টোরটি উপলব্ধ হলে, উপলব্ধ কেনাকাটাগুলো লোড করুন। পূর্ববর্তী গুগল প্লে এবং অ্যাপ স্টোর সেটআপ অনুযায়ী, storeKeyConsumable , storeKeySubscription, এবং storeKeyUpgrade দেখতে পাওয়ার আশা করা যায়। যখন কোনো প্রত্যাশিত কেনাকাটা উপলব্ধ না থাকে, তখন এই তথ্যটি কনসোলে প্রিন্ট করুন; আপনি এই তথ্যটি ব্যাকএন্ড সার্ভিসে পাঠাতেও চাইতে পারেন।

await iapConnection.queryProductDetails(ids) মেথডটি সেইসব আইডি ফেরত দেয় যেগুলো খুঁজে পাওয়া যায়নি এবং সেইসব ক্রয়যোগ্য পণ্য যেগুলো খুঁজে পাওয়া গেছে। UI আপডেট করার জন্য রেসপন্স থেকে 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(),
    );
  }
}

যদি অ্যান্ড্রয়েড এবং আইওএস স্টোরগুলো সঠিকভাবে কনফিগার করা থাকে, তবে আপনি সেখানে উপলব্ধ পণ্যগুলো দেখতে পাবেন। মনে রাখবেন, সংশ্লিষ্ট কনসোলগুলোতে কেনাকাটার তথ্য প্রবেশ করানোর পর তা উপলব্ধ হতে কিছুটা সময় লাগতে পারে।

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 ভেরিয়েবলটি তৈরি করুন এবং 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);
    }
  }

৯. ব্যাকএন্ড সেট আপ করুন

ক্রয় ট্র্যাক ও যাচাই করার কাজে অগ্রসর হওয়ার আগে, এই কাজটি সমর্থন করার জন্য একটি ডার্ট ব্যাকএন্ড সেট আপ করুন।

এই অংশে, রুট হিসেবে dart-backend/ ফোল্ডার থেকে কাজ করুন।

নিশ্চিত করুন যে আপনার নিম্নলিখিত টুলগুলি ইনস্টল করা আছে:

বেস প্রকল্পের সংক্ষিপ্ত বিবরণ

যেহেতু এই প্রজেক্টের কিছু অংশ এই কোডল্যাবের আওতার বাইরে বলে বিবেচিত, তাই সেগুলো স্টার্টার কোডে অন্তর্ভুক্ত করা হয়েছে। কাজ শুরু করার আগে স্টার্টার কোডে যা আছে তা একবার দেখে নেওয়া ভালো, যাতে আপনি বিষয়গুলো কীভাবে সাজাবেন সে সম্পর্কে একটি ধারণা পেতে পারেন।

এই ব্যাকএন্ড কোডটি আপনার মেশিনে স্থানীয়ভাবে চালানো যায়, এটি ব্যবহার করার জন্য আপনাকে এটি ডিপ্লয় করার প্রয়োজন নেই। তবে, আপনার ডেভেলপমেন্ট ডিভাইস (অ্যান্ড্রয়েড বা আইফোন) থেকে সেই মেশিনে সংযোগ করতে সক্ষম হতে হবে যেখানে সার্ভারটি চলবে। এর জন্য, ডিভাইস দুটিকে একই নেটওয়ার্কে থাকতে হবে এবং আপনার মেশিনের আইপি অ্যাড্রেসটি জানতে হবে।

নিম্নলিখিত কমান্ডটি ব্যবহার করে সার্ভারটি চালানোর চেষ্টা করুন:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

ডার্ট ব্যাকএন্ড এপিআই এন্ডপয়েন্টগুলো পরিবেশন করার জন্য shelf এবং shelf_router ব্যবহার করে। ডিফল্টরূপে, সার্ভার কোনো রাউট প্রদান করে না। পরবর্তীতে আপনি ক্রয় যাচাইকরণ প্রক্রিয়াটি পরিচালনা করার জন্য একটি রাউট তৈরি করবেন।

স্টার্টার কোডে আগে থেকেই অন্তর্ভুক্ত একটি অংশ হলো lib/iap_repository.dart এ থাকা IapRepository । যেহেতু ফায়ারস্টোর বা সাধারণভাবে ডেটাবেসের সাথে কীভাবে কাজ করতে হয় তা শেখা এই কোডল্যাবের জন্য প্রাসঙ্গিক বলে মনে করা হয় না, তাই স্টার্টার কোডে ফায়ারস্টোরে পারচেজ তৈরি বা আপডেট করার জন্য ফাংশন এবং সেই পারচেজগুলোর জন্য প্রয়োজনীয় সমস্ত ক্লাস অন্তর্ভুক্ত রয়েছে।

ফায়ারবেস অ্যাক্সেস সেট আপ করুন

Firebase Firestore অ্যাক্সেস করার জন্য আপনার একটি সার্ভিস অ্যাকাউন্ট অ্যাক্সেস কী প্রয়োজন। এটি তৈরি করতে, Firebase প্রজেক্ট সেটিংস খুলুন এবং সার্ভিস অ্যাকাউন্টস বিভাগে যান, তারপর 'Generate new private key' নির্বাচন করুন।

27590fc77ae94ad4.png

ডাউনলোড করা JSON ফাইলটি assets/ ফোল্ডারে কপি করুন এবং এর নাম পরিবর্তন করে service-account-firebase.json রাখুন।

গুগল প্লে অ্যাক্সেস সেট আপ করুন

কেনাকাটা যাচাই করার জন্য প্লে স্টোরে প্রবেশ করতে, আপনাকে এই অনুমতিগুলোসহ একটি সার্ভিস অ্যাকাউন্ট তৈরি করতে হবে এবং এর জন্য JSON ক্রেডেনশিয়ালগুলো ডাউনলোড করতে হবে।

  1. গুগল ক্লাউড কনসোলে গুগল প্লে অ্যান্ড্রয়েড ডেভেলপার এপিআই পৃষ্ঠাটি পরিদর্শন করুন। 629f0bd8e6b50be8.png যদি গুগল প্লে কনসোল আপনাকে কোনো প্রজেক্ট তৈরি করতে বা বিদ্যমান কোনো প্রজেক্টের সাথে লিঙ্ক করতে বলে, তাহলে প্রথমে তা করুন এবং তারপর এই পৃষ্ঠায় ফিরে আসুন।
  2. এরপর, সার্ভিস অ্যাকাউন্টস পেজে যান এবং + ক্রিয়েট সার্ভিস অ্যাকাউন্ট-এ ক্লিক করুন। 8dc97e3b1262328a.png
  3. সার্ভিস অ্যাকাউন্টের নাম প্রবেশ করান এবং তৈরি করুন ও চালিয়ে যান-এ ক্লিক করুন। 4fe8106af85ce75f.png
  4. পাব/সাব সাবস্ক্রাইবার ভূমিকা নির্বাচন করুন এবং সম্পন্ন ক্লিক করুন। a5b6fa6ea8ee22d.png
  5. অ্যাকাউন্ট তৈরি হয়ে গেলে ম্যানেজ কীজ- এ যান। eb36da2c1ad6dd06.png
  6. অ্যাড কী > ক্রিয়েট নিউ কী নির্বাচন করুন। e92db9557a28a479.png
  7. একটি JSON কী তৈরি ও ডাউনলোড করুন। 711d04f2f4176333.png
  8. ডাউনলোড করা ফাইলটির নাম পরিবর্তন করে service-account-google-play.json, রাখুন এবং এটিকে assets/ ডিরেক্টরিতে সরিয়ে নিন।
  9. এরপর, প্লে কনসোলের ব্যবহারকারী এবং অনুমতি পৃষ্ঠায় যান। 28fffbfc35b45f97.png
  10. ‘নতুন ব্যবহারকারীদের আমন্ত্রণ জানান’ (Invite new users) এ ক্লিক করুন এবং পূর্বে তৈরি করা সার্ভিস অ্যাকাউন্টের ইমেল ঠিকানাটি লিখুন। আপনি ‘সার্ভিস অ্যাকাউন্টস’ (Service accounts) পেইজের টেবিলে ইমেলটি খুঁজে পাবেন। e3310cc077f397d.png
  11. অ্যাপ্লিকেশনটির জন্য আর্থিক তথ্য দেখার এবং অর্ডার ও সাবস্ক্রিপশন পরিচালনা করার অনুমতি প্রদান করুন। a3b8cf2b660d1900.png
  12. ব্যবহারকারীকে আমন্ত্রণ জানান- এ ক্লিক করুন।

আমাদের আরও একটি কাজ করতে হবে lib/constants.dart, খুলে androidPackageId এর মানটি আপনার অ্যান্ড্রয়েড অ্যাপের জন্য বেছে নেওয়া প্যাকেজ আইডি দিয়ে প্রতিস্থাপন করা।

অ্যাপল অ্যাপ স্টোর অ্যাক্সেস সেট আপ করুন

কেনাকাটা যাচাই করার জন্য অ্যাপ স্টোরে প্রবেশ করতে, আপনাকে একটি শেয়ার্ড সিক্রেট সেট আপ করতে হবে:

  1. অ্যাপ স্টোর কানেক্ট খুলুন।
  2. আমার অ্যাপস- এ যান এবং আপনার অ্যাপটি নির্বাচন করুন।
  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 .