Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
消費型プロダクトのGoogle Play Billing Library移行
Search
Yukio MURAKAMI
May 17, 2021
Programming
0
250
消費型プロダクトのGoogle Play Billing Library移行
集まれKotlin好き!Kotlin愛好会 vol.29 @オンライン
Yukio MURAKAMI
May 17, 2021
Tweet
Share
More Decks by Yukio MURAKAMI
See All by Yukio MURAKAMI
モーダル表示について
m_yukio
0
190
Kotlin初心者がGoogle Play Billing Libraryを使ってみた
m_yukio
0
290
Other Decks in Programming
See All in Programming
코딩 에이전트 체크리스트: Claude Code ver.
nacyot
0
930
AI時代のソフトウェア開発を考える(2025/07版) / Agentic Software Engineering Findy 2025-07 Edition
twada
PRO
99
37k
ISUCON研修おかわり会 講義スライド
arfes0e2b3c
1
470
Vibe Codingの幻想を超えて-生成AIを現場で使えるようにするまでの泥臭い話.ai
fumiyakume
10
4.6k
AI駆動のマルチエージェントによる業務フロー自動化の設計と実践
h_okkah
0
230
React は次の10年を生き残れるか:3つのトレンドから考える
oukayuka
15
4.6k
ソフトウェア品質を数字で捉える技術。事業成長を支えるシステム品質の マネジメント
takuya542
2
15k
PHP 8.4の新機能「プロパティフック」から学ぶオブジェクト指向設計とリスコフの置換原則
kentaroutakeda
2
1k
型で語るカタ
irof
0
700
Model Pollution
hschwentner
1
160
顧客の画像データをテラバイト単位で配信する 画像サーバを WebP にした際に起こった課題と その対応策 ~継続的な取り組みを添えて~
takutakahashi
4
1.3k
Git Sync を超える!OSS で実現する CDK Pull 型デプロイ / Deploying CDK with PipeCD in Pull-style
tkikuc
4
350
Featured
See All Featured
Into the Great Unknown - MozCon
thekraken
40
1.9k
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
The Pragmatic Product Professional
lauravandoore
35
6.7k
Designing for humans not robots
tammielis
253
25k
Raft: Consensus for Rubyists
vanstee
140
7k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
26
2.9k
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.4k
Site-Speed That Sticks
csswizardry
10
700
Imperfection Machines: The Place of Print at Facebook
scottboms
267
13k
The Power of CSS Pseudo Elements
geoffreycrofte
77
5.9k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
126
53k
StorybookのUI Testing Handbookを読んだ
zakiyama
30
5.9k
Transcript
Ϗοπ༗ݶձࣾଜ্༤ ফඅܕϓϩμΫτͷ (PPHMF1MBZ#JMMJOH-JCSBSZҠߦ *OBQQ#JMMJOH "*%- ͔ΒͷҠߦ
$PDPBTXJGU w ାίϫʔΩϯάεϖʔε0QFO0 ff i DF'03&45Ͱ։࠵ͯ͠·͢ɻ w NBD04J04XBUDI04UW04"OESPJEϓϩάϥϚʔͷͨΊͷษڧ ձͰ͢ɻ w
ϓϩάϥϜʹؔ͢ΔࣝܦݧΛڞ༗Λతͱ͍ͯ͠·͢ɻ w ձͷ׆ಈ༰ൃදࢿྉɺαϯϓϧίʔυެ։Λલఏͱ͍ͯ͠· ͢ɻ w ਃ͠ࠐΈɺDPOOQBTT͔Βɻ https://cocoa-kanto.connpass.com/
࡛ۄݝேբࢢͰιϑτϋεΛىۀɻ NBD04J04ɺ"OESPJEͷΞϓϦέʔγϣϯ։ൃΛओʹ͚ෛ͏ɻ ࣗࣾΞϓϦͷ࡞ɻ 5XJUUFS!N@ZVLJP 'BDFCPPLZVLJPNVSBLBNJ (JU)VCNVSBLBNJ IUUQXXXCJU[DPKQXFCMPH
w"OESPJEΠϯλʔϑΣʔεఆٛݴޠʢ"*%-ʣΛ͍ͬͯΔ*O BQQ#JMMJOH͔Β(PPHMF1MBZ#JMMJOH-JCSBSZͷҠߦ͕ඞਢͱ ͳ͍ͬͯ·͢ɻ w(PPHMF1MBZ#JMMJOH-JCSBSZରԠࡁΈͰɺWWͷ߹ ɺWͷҠߦ͕ඞਢͱͳ͍ͬͯ·͢ɻ w৽نΞϓϦ͔Βɻ wطଘΞϓϦ·Ͱʹɻ
*OBQQ#JMMJOH࣌ w ఏڙ͞ΕΔ"1*؆ૉͳ͍ͷͷɻ w αϯϓϧΛϦϑΝϨϯε࣮ͱͯ͠׆༻ɻ https://github.com/android/play-billing-samples/ w "1*ͦͷͷͷͰͳ͍͕՝͕͋ͬͨɻ w
"DUJWJUZ'SBHNFOU࠶ੜͷࡍʹɺྫ͑ϝϯόʔͱͯ͠ อ͍࣋ͯͨ͠՝ۚίʔυ͕ࡴ͞ΕɺͦΕʹΑͬͯΤϥʔ͕ൃ ੜ͢Δɻ w ྫ͑ը໘ճసɻΞϓϦ͕ը໘ճసΛ༗ޮʹ͍ͯͨ͘͠ ͯɺγεςϜͰը໘ճసΛ༗ޮʹ͍ͯ͠Δͱൃੜ͢Δ ύλʔϯ͕͋Δɻ
(PPHMF1MBZ#JMMJOH-JCSBSZͷҠߦ w ߴ͍ͷ"1* w *OBQQ#JMMJOH αϯϓϧͱ༷ʹࠩҟ͕ൃੜ͢Δɻ w อཹதͷऔҾͷରԠ͕ඞਢͱͳΔɻ w "1*ͦͷͷͷརͰͳ͍͕ศརͳϥΠϒϥϦ͕ར༻Ͱ͖Δɻ
w "DUJWJUZ'SBHNFOU࠶ੜͷରԠɻ w -JWF%BUB "DUJWJUZ'SBHNFOUͷΠϯελϯεͷอ͕࣋ආΒΕΔɻ w 7JFX.PEFM "DUJWJUZ'SBHNFOUͷण໋ͱಠཱͯ͠ଘࡏͰ͖Δɻ ϓϩηεͷ࠶ੜੑͷରԠͰ͖ͳ͍͕ɻ
ফඅܕϓϩμΫτͷ߹ͷରԠ߲ w ॳظԽͱଓ w ΞΠςϜͷใΛऔಘ͢Δ w ߪೖ͢Δ w ߪೖͷใΛऔಘ͢Δ w
ফඅ͢Δ
αϯϓϧ5SJWJBM%SJWF,PUMJOͷΫϥεߏ (BNF'SBHNFOU #JMMJOH7JFX.PEFM #JMMJOH3FQPTJUPSZ ࠷৽ͷαϯϓϧ,PUMJO'MPXΛͬͨ#JMMJOH%BUB4PVSDFΫϥεʹ՝ۚϩδοΫΛ࣋ͬͯߦ͍ͬͯ·͢ɻ
ॳظԽͱଓ class BillingRepository private constructor( private val application: Application, private
val coroutineScope: CoroutineScope) : PurchasesUpdatedListener, BillingClientStateListener { lateinit private var billingClient: BillingClient fun startDataSourceConnections() { billingClient = BillingClient.newBuilder(application.applicationContext) .setListener(this) .enablePendingPurchases() .build() connectToPlayBillingService() } private fun connectToPlayBillingService() { billingClient.startConnection(this) } } อཹதͷऔҾΛ༗ޮʹ͢Δ
ΞΠςϜͷใΛऔಘ͢Δ ͷ4,6ͷϦετ ఆظߪೖͰͳ͍ fun querySkuDetailsAsync(skuList: List<String>) { val skuType: String
= BillingClient.SkuType.INAPP val params = SkuDetailsParams.newBuilder().setSkusList(skuList).setType(skuType).build() billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList -> when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { if (skuDetailsList.orEmpty().isNotEmpty()) { // ߪೖՄೳͳΞΠςϜҰཡΛऔಘ } else { // ޭ͕ۭͨͩͬͨ͠ } } else -> { // Τϥʔ } } } }
ߪೖ͢Δ fun launchBillingFlow(activity: Activity, sku: String) = coroutineScope.launch { val
skuType: String = BillingClient.SkuType.INAPP val params = SkuDetailsParams.newBuilder().setSkusList(listOf(sku)).setType(skuType).build() billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList -> when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { if (skuDetailsList.orEmpty().isNotEmpty()) { val purchaseParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetailsList!!.first()).build() val result = billingClient.launchBillingFlow(activity, purchaseParams) if (result.responseCode != BillingClient.BillingResponseCode.OK) { /* Google PlayٻαʔϏεͷߪೖ͕ؔΤϥʔΛฦͨ͠ */ } } else { /* ࢦఆ͞ΕͨΞΠςϜIDͷৄࡉ͕ଘࡏ͠ͳ͔ͬͨ */ } } else -> { /* ࢦఆ͞ΕͨΞΠςϜIDͷৄࡉͷऔಘ͕ࣦഊͨ͠ */ } } } } override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) { when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { if (purchases != null) { if (isPending(purchases)) { /* อཹதͷऔҾ */ } else { /* ߪೖޭ */ } } else { /* ޭ͕ฦ͞Ε͕ͨɺߪೖͨ͠Ϧετ͕nullͩͬͨɻ */ } } BillingClient.BillingResponseCode.USER_CANCELED -> { /* Ωϟϯηϧ͞Εͨ */ } BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> { /* ߪೖ͚ͨ͠Ͳফඅ͞Ε͍ͯͳ͍ */ } BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> { /* ࠶ଓΛࢼΈΔ */ connectToPlayBillingService() } else -> { /* ͦͷଞΤϥʔ */ } } } private fun isPending(purchases: List<Purchase>): Boolean { purchases.forEach { if (it.getPurchaseState() != Purchase.PurchaseState.PURCHASED) { return true } } return false }
ߪೖͷใΛऔಘ͢Δ fun queryPurchasesAsync() = coroutineScope.launch { val skuType: String =
BillingClient.SkuType.INAPP val purchasesResult = billingClient.queryPurchases(skuType) val validPurchases = mutableListOf<Purchase>() purchasesResult?.purchasesList?.let { it.forEach { purchase -> if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { validPurchases.add(purchase) } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) { // อཹதͷऔҾ } } } // validPurchases : ະফඅʢফඅࡁΈʣϦετऔಘ }
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/ {packageName}/purchases/products/{productId}/tokens/{token}:acknowledge { "developerPayload": string } .FUIPEQVSDIBTFTQSPEVDUTBDLOPXMFEHF IUUQTEFWFMPQFSTHPPHMFDPNBOESPJEQVCMJTIFSBQJSFGSFTUWQVSDIBTFTQSPEVDUTBDLOPXMFEHF IMFO 3FTQPOTFCPEZ
*GTVDDFTTGVM UIFSFTQPOTFCPEZJTFNQUZ
fun handleConsumablePurchasesAsync(purchaseToken: String) = coroutineScope.launch { val skuType: String =
BillingClient.SkuType.INAPP var purchase: Purchase? = null val purchasesResult = billingClient.queryPurchases(skuType) purchasesResult?.purchasesList?.let { it.forEach { if ((it.purchaseToken == purchaseToken) && (it.getPurchaseState() == Purchase.PurchaseState.PURCHASED)) { purchase = it return@forEach } } } if (purchase == null) { return@launch } val params = ConsumeParams.newBuilder().setPurchaseToken(purchase!!.purchaseToken).build() billingClient.consumeAsync(params) { billingResult, purchaseToken -> when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { /* ফඅޭ */ } else -> { /* ͦͷଞΤϥʔ */ } } } } ফඅ͢Δ
wߪೖͷߋ৽ใʹɺߪೖࡁΈͱอཹதͷ͕ଘࡏ͢Δɻ wߪೖॲཧͷظݶ͕ઃ͚ΒΕͨɻ wɻςετڥͰͩͬͨɻ wඇফඅܕϓϩμΫτͰɺظݶʹ #JMMJOH$MJFOUBDLOPXMFEHF1VSDIBTF ͕ݺΕͳ͍ͱฦۚͱ ͳΔɻ w(PPHMF1MBZ%FWFMPQFS"1*Λͬͨߪೖͷεςʔλεͷ֬ೝ͕ ॏཁͱͳΔɻ