はじめに
こんにちは!mikanでAndroidエンジニアをしている、gumiojiです!
こちらはmikanのアドベントカレンダー 18日目の記事になります!
1つ前の17日目の記事は@chiakiの「CSがQAやったら意外とメリット多かった話」でした!
CSでmikanに入社したchiakiさんがQAに転身したのですが、ほんとに観点が鋭くて日々すごく助かってますw
余談ですが、この頼もしいQAチームのおかげで、上位プラン導入はリリース後、驚くほどに何も問題がなくて逆に怖いくらいです…w
今回mikanでは、今までの通常プランのmikan PROの上位プランにあたる、PRO PLUSを導入する際にAndroidの課金ライブラリである、Billing Libraryを最新のv5に同時にアップデートしたので、その際の辛かったことポイント2点を軽い備忘録として残そうかなと思います!
PRO PLUSとは? どういった背景で導入したの?という方は前回の僕の振り返り記事のPRO PLUS対応欄をご覧ください!!
対象読者
これからAndroidでBilling Library v5を新規で導入したい方、v4利用中で移行したい方
Billing Library
Google Playの課金システムにアプリから楽にアクセスできるLibraryと思ってもらえたらと思います!2022年12月16日現時点ではVersion5が最新です!
いつも最新版は2年間のサポートとなりますので、v5は再来年の5月までには利用しておきましょう!
新規導入の方
サクッと確認したい方、手元にPCなどがない場合は公式の導入ガイドを見ていただければと思います!
既存から移行する方
こちらも移行ガイドを用意してくれているので、それに従いましょう!
共通: 手を動かしながら確認したい方
お時間がある方、手元にPCがある方はCodelabsが用意されているので、実際にコーディングなどを通じて学んでいくと良いと思います
補足: Google codelabsとは?
Googleが用意している、各お題毎にガイド付きのチュートリアルを用意したハンズオン形式のコーディング体験ができるコンテンツです!
codelabs.developers.google.com
それでは実際に何が辛かったのかの紹介をしていこうと思います!
実装でハマった部分
つらみ1: 新しい構造の扱いにくさ
v5で一番大きな変化といえば、課金モデルが変わったことです。
Before: v4まで
- 1Subscription(aPlan + aOffer)
After: v5から
- 1Subscription
- aPlan
- aOffer
- bOffer
- bPlan
- ・・・
- cPlan
- aPlan
図で表現すると以下のような形になり、左側がBeforeで、右側がAfterとなります
Beforeは全てが1つの塊として作成されてしまっていたため、柔軟性がなく、別期間を用意したい、特別な割引などを別途提供したいなどの際に1つ1つ新しいSubscriptionを作成してました。
ex.
- Subscription
- mikan PRO 1ヶ月
- Subscription
- mikan PRO 6ヶ月
- Subscription
- mikan PRO 1年間
- Subscription
- mikan PRO 1年間
- 半額セール
- mikan PRO 1年間
Afterからは階層的な構造になったことで、柔軟性が増しました。
今までと変わらず、一番上にSubscriptionが存在し、その下に期間などを表すBasePlanが存在し、最後に割引や無料トライアル等といった提供を行うOfferが存在します
ex. 以下のような形になり、新しい期間やセールなどをを追加したい、変更したい場合に追加することが可能です
- Subscription
- mikan PRO
- 1ヶ月
- 無料トライアル
- 6ヶ月
- 無料トライアル
- 1年間
- 無料トライアル
- 半額セール
- 1ヶ月
- mikan PRO
お〜!いいやん!って思ったんですけどね、BasePlanとかOfferの取得どこ???
そんなものはないぜ!!!全部tagで指定しろよな!!とのことらしいです。。。。。
「t…tag….? なんですかいそれは…?」
BasePlanやOfferなどはコード上で取得をする際にtagを使って取得するように作られています。
実際にplay consoleの画面を見てみましょう!画像内のタグという箇所になります。
BasePlan
Offer
ここまではまだいいんですが、BasePlanのtagを指定したのはいいけど、そのtagはどこで使うんですかい?? どこで指定するんですかい?? なくないですか?? Googleさん??()
これに関しては、ProductDetails.SubscriptionOfferDetailsのofferTagsにbasePlanもofferのtagもまとめて入ってくるみたいです….そんなこと1mmも書いてないんですけど….(せめて分けるか分かりやすいのを用意してほしかったよ…offerって書いてますやん…)
この辺どこに情報が隠れてるかというと、Codelabsのsample githubに書いてあります
上記sampleより、以下指定されたtagをofferDetailsの中から該当するものがあるか、あればそのofferDetailsを返すものです
/** * Retrieves all eligible base plans and offers using tags from ProductDetails. * * @param offerDetails offerDetails from a ProductDetails returned by the library. * @param tag string representing tags associated with offers and base plans. * * @return the eligible offers and base plans in a list. * */ private fun retrieveEligibleOffers( offerDetails: MutableList<ProductDetails.SubscriptionOfferDetails>, tag: String ): List<ProductDetails.SubscriptionOfferDetails> { val eligibleOffers = emptyList<ProductDetails.SubscriptionOfferDetails>().toMutableList() offerDetails.forEach { offerDetail -> if (offerDetail.offerTags.contains(tag)) { eligibleOffers.add(offerDetail) } } return eligibleOffers }
上記のtagを渡してる呼び出し元は以下で、tag = MONTHLY_BASIC_PLANS_TAG
って書いてありますね….
viewModel.buy( productDetails = it, currentPurchases = currentPurchases, tag = MONTHLY_BASIC_PLANS_TAG, activity = activity )
う〜〜〜〜ん、めんどくさいっ()
mikanではどうしたか
実は既存の課金アイテムはv5がリリースされた際に強制的に以下のような新しい形に強制変換されており、新規で追加するプランではあったのですがこの形に寄せました!
理由は扱いにくかったのとプランはそんな頻繁に追加はしないためです。
Offerは頻繁に追加したくなると思うので、そこだけ恩恵を受けれれば良いと考えました。
そのため、今回新しく追加したPRO PLUSには本来は3つのbasePlanが存在しますが、1Subscription: 1BasePlanとして扱ってるため、3つのSubscriptionとして作成しています!
つらみ2: Offerの融通の効かなさ
今回新たに追加されたOfferの選択ですが、こちらはBillingFlowParams.ProductDetailsParams.BuilderのsetOfferTokenにofferDetails.offerTokenを渡してあげることで指定できます!
先程上記で紹介したように選択すべきofferをtagを利用し、複数のofferDetailsから見つけ出して上げる必要があります。
val eligibleOffers = emptyList<ProductDetails.SubscriptionOfferDetails>().toMutableList() offerDetails.forEach { offerDetail -> if (offerDetail.offerTags.contains(tag)) { eligibleOffers.add(offerDetail) } } return eligibleOffers
見つけたofferのtokenをbillingParamsのofferTokenに渡すのですが、offerなんて提供してないよ!だからいらないんじゃない??って方も空でいいので呼び出してあげてください。じゃないと機能してくれません
val billingParams = billingFlowParamsBuilder( productDetails = productDetails, offerToken = offerToken // 必ず呼ぶんじゃぞ )
なんか動かないな〜と思ったら、空でよいので呼び出してあげてください
→ ちなみに2022/08時点では、空をセットした場合は内部でoffer適用含め、最も安い価格になる条件になってました
このofferTagなんですが、消化した後も反映されるまでに少し時間がかかり、流れてくるのが混乱を招きました
→ 現時点ではこの辺りが改善されてる可能性もあるので、ご了承くださいm
offerはtokenだけを渡して、後はよしなにplay consoleがハンドリングしてくれるので一度消費していたら、実際の金額請求に関しては問題ないのですが、offerを提供しているのか? もう提供済みなのか? のLogなどを取りたかったため、この辺りは少し困りました。
→ これに関して、金額も一緒で複数のフェーズに分かれて、全部流れてきてしまい、反映されるまではoffer消費していてもoffer提供価格の金額が流れてくることがありました
じゃあ、バックエンドに頼って、バックエンド側でLogを送ってもらry
→ offer提供かどうかなんて判断できひんで^^
・・・・
mikanではどうしたか
あまり良くないとは思いますが、以下のようにしました
- 無料トライアルはアプリ単位で1回のみなんで、起動時にplay storeの履歴を確認し、1度でも購入してるかをみる
- 限定オファーもアプリ単位で1回のみなんで、起動時にplay storeの履歴を確認し、1度でも対象のItem群の内のどれか1つでも購入してるかをみる
この状態で条件を満たさない限りはofferTagを選択させないという状態にすることで消費されたかどうかを実現させています。
また、先程も述べたようにdefaultだと最低価格計算になってしまうため、offerを未適用にさせるという処理も追加する必要がありました。
→ 限定オファーの条件を満たしてないけど、まだ消費してない場合といったビジネス上の条件が存在する場合に当てはまります
実際のoffer選択の処理は以下のようにしています
var selectedOfferDetails: SubscriptionOfferDetails? = null productDetails.subscriptionOfferDetails.orEmpty().forEach{offerDetails-> if (selectMode != null) { offerDetails.offerTags.forEach{tags-> if (tags.contains(selectMode.offerTag)) { selectedOfferDetails = offerDetails } } } else { if (offerDetails.offerTags.isEmpty()) selectedOfferDetails = offerDetails }
未適用 = 通常価格の場合はofferTagsが空っぽのため、空っぽのofferDetailsをセットして返してあげることで実現できてます!!
この辺りのofferのいい感じの処理の仕方、何かいい方法を知ってる方いれば、よろしくお願いします!!!
スペシャルサンクス
今回、mikanのBilling Libraryをv5にアップデートでき、無事何事もなくスムーズに上位プランであるPRO PLUSを導入できたのも、業務委託で手伝ってくださってるsyarihuさんのおかげです!
課金基盤の準備、アップデート作業、v5の調査や実装過程のアドバイスなどをしてくださってました!改めて、本当にありがとうございます!
おわりに
いかがだったでしょうか? まだ最新版に上げてないよ~って方や、これから使ってみようかなという方の参考になればと思います!
さて、mikanでは最高の英語アプリを共に作っていける仲間を常に探しております!(特にデザイナー…!!)
お気軽にTwitterでも下記のHERP経由でもご連絡ください。主はSplatoonが大好きなのでSplatoon面談もWelcomeです!
明日は@yocchanによる、「なぜ英語を勉強するのか-私が考えるmikanで人生の可能性が広がる理由」です!お楽しみに!