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
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Yukio MURAKAMI
May 17, 2021
Programming
0
270
消費型プロダクトの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
310
Other Decks in Programming
See All in Programming
AI Schema Enrichment for your Oracle AI Database
thatjeffsmith
0
250
AI Agent Tool のためのバックエンドアーキテクチャを考える #encraft
izumin5210
6
1.8k
今から始めるClaude Code超入門
448jp
7
8.5k
副作用をどこに置くか問題:オブジェクト指向で整理する設計判断ツリー
koxya
1
590
Vibe Coding - AI 驅動的軟體開發
mickyp100
0
170
Basic Architectures
denyspoltorak
0
660
登壇資料を作る時に意識していること #登壇資料_findy
konifar
4
980
CSC307 Lecture 04
javiergs
PRO
0
660
Smart Handoff/Pickup ガイド - Claude Code セッション管理
yukiigarashi
0
130
開発者から情シスまで - 多様なユーザー層に届けるAPI提供戦略 / Postman API Night Okinawa 2026 Winter
tasshi
0
200
360° Signals in Angular: Signal Forms with SignalStore & Resources @ngLondon 01/2026
manfredsteyer
PRO
0
120
AI巻き込み型コードレビューのススメ
nealle
0
130
Featured
See All Featured
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
110
Code Reviewing Like a Champion
maltzj
527
40k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
430
Understanding Cognitive Biases in Performance Measurement
bluesmoon
32
2.8k
エンジニアに許された特別な時間の終わり
watany
106
230k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
22k
Practical Orchestrator
shlominoach
191
11k
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
240
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
53
HDC tutorial
michielstock
1
360
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*Λͬͨߪೖͷεςʔλεͷ֬ೝ͕ ॏཁͱͳΔɻ