はじめに
アプリ開発において、マネタイズを考えることは多いと思います。
ですが、いざやろうと思っても「参考になる記事やドキュメントが見つからない!」という経験はありませんか?
僕自身も個人開発で作っている「FoodGram」のアプリで、いつかお小遣いが稼げたらいいなと思っていました。
今回はiOS限定になりますが、サブスクの導入方法や試行錯誤の過程をまとめてみました。
個人開発のちょっとした紹介
個人開発で「FoodGram」というアプリを開発しています。
FoodGramは、世界中のユーザーと食の楽しさを共有できるフードシェアアプリです。美味しい料理やお気に入りのグルメスポットを記録・共有しながら、みんなでグローバルな「フードマップ」を作りましょう🍜。
「FoodGram」は、フードシェアアプリとなっており、あなたの好きなレストランの食事をぜひこのアプリで共有していただけると嬉しいです!!
サブスクを実装するにあたって
僕自身もサブスクを実装したいと思った理由としては、去年のFlutter Kaigiにて、以下の発表がありました。
登壇内容に感銘を受けて、登壇した方に直接質問をしたり、自分のアプリについて話をしてマネタイズについて、たくさん教えていただきました。英語だったので会話も大変でしたが、自分にとってアプリ開発の意識が変わった瞬間でした。
次のFlutterKaigiでは、サブスク関連の話をしたいものですね。
すみません。横道逸れました。ということで僕は「RevenueCat」というサービスを活用させていただきました。
RevenueCatとは
アプリ内課金(IAP)やサブスクリプションを簡単に導入・管理できるサービスになります。クロスプラットフォーム対応で、iOS、Android、Webアプリなど様々なプラットフォームで利用できます。
実装する前に
実装するまえに導入する方法があります。以下の記事を参考にさせていただきました。これらについてはSKIPさせていただきます。
Flutter側で実装をする
1.purchases_flutterのインストールする
flutter pub add purchases_flutter
2.実装していく
コードですが、先ほど紹介した方のコードをほぼ参考にさせていただきました。それを自分で使いやす
く改造してみました。
サブスクの登録の処理を記載するProvider
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:food_gram_app/env.dart';
import 'package:food_gram_app/main.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'purchase_provider.g.dart';
@riverpod
class Purchase extends _$Purchase {
bool isSubscribed = false;
late Offerings offerings;
final user = supabase.auth.currentUser?.id;
@override
Future<bool> build() {
return initInAppPurchase();
}
Future<bool> initInAppPurchase() async {
try {
late PurchasesConfiguration configuration;
if (Platform.isAndroid) {
// 難読化したAPIKEYを入れる
configuration = PurchasesConfiguration(Env.androidPurchaseKey);
} else if (Platform.isIOS) {
// 難読化したAPIKEYを入れる
configuration = PurchasesConfiguration(Env.iOSPurchaseKey);
}
await Purchases.configure(configuration);
/// Offerings を取得
offerings = await Purchases.getOfferings();
/// Supabase の UID を使用してログイン
final result = await Purchases.logIn(user!);
await getPurchaserInfo(result.customerInfo);
/// アクティブなアイテムをログ出力
final isSubscription = result.customerInfo.entitlements.active;
if (isSubscription.isEmpty) {
return false;
} else {
return true;
}
} on PlatformException catch (e) {
logger.e('initInAppPurchase error caught! $e');
return false;
}
}
Future<void> getPurchaserInfo(CustomerInfo customerInfo) async {
try {
isSubscribed =
await updatePurchases(customerInfo, 'monthly_subscription');
} on PlatformException catch (e) {
logger.e('getPurchaserInfo error $e');
}
}
Future<bool> updatePurchases(
CustomerInfo purchaserInfo,
String entitlement,
) async {
var isPurchased = false;
final entitlements = purchaserInfo.entitlements.all;
if (entitlements.isEmpty || !entitlements.containsKey(entitlement)) {
isPurchased = false;
} else if (entitlements[entitlement]!.isActive) {
isPurchased = true;
}
return isPurchased;
}
/// 購入処理
/// makePurchase()を呼び出して実際に課金処理を行う
Future<bool> makePurchase(String offeringsName) async {
await initInAppPurchase();
try {
Package? package;
package = offerings.all[offeringsName]?.monthly;
if (package != null) {
await Purchases.logIn(user!);
final customerInfo = await Purchases.purchasePackage(package);
await getPurchaserInfo(customerInfo);
return true;
}
return false;
} on PlatformException catch (e) {
logger.e('makePurchase error $e');
return false;
}
}
/// 購入の復元
/// iosの場合は、購入の復元(以前の購入履歴を復元する)を実装することが必要
Future<bool> restorePurchase(String entitlement) async {
try {
/// Entitlements
/// 「アイテムの保有状況(アイテムが購入済みで、アクティブになっているかどうか)」を確認するための設定項目
final customerInfo = await Purchases.restorePurchases();
final isActive = await updatePurchases(customerInfo, entitlement);
if (!isActive) {
logger.w('購入情報なし');
return false;
} else {
await getPurchaserInfo(customerInfo);
logger.i('$entitlement 購入情報あり 復元可能');
return true;
}
} on PlatformException catch (e) {
logger.e('purchase repo restorePurchase error $e');
return false;
}
}
}
サブスクしているかどうかを管理するProvider
import 'package:flutter/services.dart';
import 'package:food_gram_app/core/data/purchase/purchase_provider.dart';
import 'package:food_gram_app/main.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'subscription_provider.g.dart';
@riverpod
Future<bool> subscription(
SubscriptionRef ref,
) async {
try {
await ref.read(purchaseProvider.notifier).initInAppPurchase();
// RevenueCatからCustomerInfoを取得
final customerInfo = await Purchases.getCustomerInfo();
// 指定されたエンタイトルメントがアクティブか確認
final entitlements = customerInfo.entitlements.all;
if (entitlements.containsKey('foodgram_premium_membership') &&
entitlements['foodgram_premium_membership']!.isActive) {
logger.i('サブスクできてる!!');
return true;
}
return false;
} on PlatformException catch (e) {
logger.e('Error checking entitlement: $e');
return false;
}
}
呼び出し例(設定画面などから)
以下のように、ユーザーがサブスクを購入したいときに呼び出します。
final result = await ref.read(settingViewModelProvider().notifier).purchase();
if (result) {
// 購入成功の処理
} else {
// エラーハンドリング
}
サブスクを登録することでできること
自分なりに考えて、あれこれ入れてみました。まだまだこれからも増やしていく予定です。
広告が表示されない | プロフィールが豪華 | プロフィールを自分の好きな画像にできる |
---|---|---|
![]() |
![]() |
![]() |
最後に
ここまで読んでくださり、ありがとうございました!
個人開発の中で「収益化」に挑戦するのは、正直とても勇気がいることでした。でも、FlutterKaigiでの出会いや、RevenueCatのような便利なサービスのおかげで、ようやく形にできそうです。
同じようにマネタイズに悩んでいる方の参考になれば嬉しいです。質問や感想があればぜひコメントください!
一緒にFlutterで楽しく開発&マネタイズしていきましょう!
おまけの宣伝
FlutterKaigiが面白い企画をやっているので、乗っからせていただきました。せっかくなのでたくさん投稿してね。