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
230
消費型プロダクトの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
170
Kotlin初心者がGoogle Play Billing Libraryを使ってみた
m_yukio
0
280
Other Decks in Programming
See All in Programming
CSC305 Lecture 26
javiergs
PRO
0
140
モバイルアプリにおける自動テストの導入戦略
ostk0069
0
110
선언형 UI에서의 상태관리
l2hyunwoo
0
150
今からはじめるAndroidアプリ開発 2024 / DevFest 2024
star_zero
0
1k
42 best practices for Symfony, a decade later
tucksaun
1
180
SymfonyCon Vienna 2025: Twig, still relevant in 2025?
fabpot
3
1.2k
テストケースの名前はどうつけるべきか?
orgachem
PRO
0
130
クリエイティブコーディングとRuby学習 / Creative Coding and Learning Ruby
chobishiba
0
3.9k
複雑な仕様に立ち向かうアーキテクチャ
myohei
0
170
Recoilを剥がしている話
kirik
5
6.6k
Symfony Mapper Component
soyuka
2
730
CSC305 Lecture 25
javiergs
PRO
0
130
Featured
See All Featured
The Language of Interfaces
destraynor
154
24k
Building a Scalable Design System with Sketch
lauravandoore
460
33k
Typedesign – Prime Four
hannesfritz
40
2.4k
Principles of Awesome APIs and How to Build Them.
keavy
126
17k
The Cult of Friendly URLs
andyhume
78
6.1k
The Cost Of JavaScript in 2023
addyosmani
45
7k
Adopting Sorbet at Scale
ufuk
73
9.1k
Scaling GitHub
holman
458
140k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
665
120k
Rebuilding a faster, lazier Slack
samanthasiow
79
8.7k
Visualization
eitanlees
146
15k
Become a Pro
speakerdeck
PRO
26
5k
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*Λͬͨߪೖͷεςʔλεͷ֬ೝ͕ ॏཁͱͳΔɻ