Slide 1

Slide 1 text

Ϗοπ༗ݶձࣾଜ্޾༤ ফඅܕϓϩμΫτͷ 
 (PPHMF1MBZ#JMMJOH-JCSBSZҠߦ *OBQQ#JMMJOH "*%- ͔ΒͷҠߦ

Slide 2

Slide 2 text

$PDPBTXJGU w ஑ାίϫʔΩϯάεϖʔε0QFO0 ff i DF'03&45Ͱ։࠵ͯ͠·͢ɻ w NBD04J04XBUDI04UW04"OESPJEϓϩάϥϚʔͷͨΊͷษڧ ձͰ͢ɻ w ϓϩάϥϜʹؔ͢Δ஌ࣝ΍ܦݧΛڞ༗Λ໨తͱ͍ͯ͠·͢ɻ w ձͷ׆ಈ಺༰΍ൃදࢿྉɺαϯϓϧίʔυ͸ެ։Λલఏͱ͍ͯ͠· ͢ɻ w ਃ͠ࠐΈ͸ɺDPOOQBTT͔Βɻ 
 https://cocoa-kanto.connpass.com/

Slide 3

Slide 3 text

࡛ۄݝேբࢢͰιϑτϋ΢εΛىۀɻ NBD04΍J04ɺ"OESPJEͷΞϓϦέʔγϣϯ։ൃΛओʹ੥͚ෛ͏ɻ ࣗࣾΞϓϦͷ੡࡞ɻ 5XJUUFS!N@ZVLJP 'BDFCPPLZVLJPNVSBLBNJ (JU)VCNVSBLBNJ IUUQXXXCJU[DPKQXFCMPH

Slide 4

Slide 4 text

w"OESPJEΠϯλʔϑΣʔεఆٛݴޠʢ"*%-ʣΛ࢖͍ͬͯΔ*O BQQ#JMMJOH͔Β(PPHMF1MBZ#JMMJOH-JCSBSZ΁ͷҠߦ͕ඞਢͱ ͳ͍ͬͯ·͢ɻ w(PPHMF1MBZ#JMMJOH-JCSBSZରԠࡁΈͰ΋ɺW΍Wͷ৔߹ ͸ɺW΁ͷҠߦ͕ඞਢͱͳ͍ͬͯ·͢ɻ w৽نΞϓϦ͸͔Βɻ wطଘΞϓϦ͸·Ͱʹɻ

Slide 5

Slide 5 text

*OBQQ#JMMJOH࣌୅ w ఏڙ͞ΕΔ"1*͸؆ૉͳ௿͍૚ͷ΋ͷɻ w αϯϓϧΛϦϑΝϨϯε࣮૷ͱͯ͠׆༻ɻ 
 https://github.com/android/play-billing-samples/ w "1*ͦͷ΋ͷͷ໰୊Ͱ͸ͳ͍͕՝୊͕͋ͬͨɻ w "DUJWJUZ'SBHNFOU࠶ੜ੒ͷࡍʹɺྫ͑͹ϝϯόʔͱͯ͠ อ͍࣋ͯͨ͠՝ۚίʔυ͕ࡴ͞ΕɺͦΕʹΑͬͯΤϥʔ͕ൃ ੜ͢Δɻ w ྫ͑͹ը໘ճసɻΞϓϦ͕ը໘ճసΛ༗ޮʹ͍ͯͨ͘͠ ͯ΋ɺγεςϜͰը໘ճసΛ༗ޮʹ͍ͯ͠Δͱൃੜ͢Δ ύλʔϯ͕͋Δɻ

Slide 6

Slide 6 text

(PPHMF1MBZ#JMMJOH-JCSBSZ΁ͷҠߦ w ߴ͍૚ͷ"1* w *OBQQ#JMMJOHαϯϓϧͱ࢓༷ʹࠩҟ͕ൃੜ͢Δɻ w อཹத΁ͷऔҾ΁ͷରԠ͕ඞਢͱͳΔɻ w "1*ͦͷ΋ͷͷར఺Ͱ͸ͳ͍͕ศརͳϥΠϒϥϦ͕ར༻Ͱ͖Δɻ w "DUJWJUZ'SBHNFOU࠶ੜ੒΁ͷରԠɻ w -JWF%BUB 
 "DUJWJUZ'SBHNFOUͷΠϯελϯεͷอ͕࣋ආΒΕΔɻ w 7JFX.PEFM 
 "DUJWJUZ'SBHNFOUͷण໋ͱ͸ಠཱͯ͠ଘࡏͰ͖Δɻ 
 ϓϩηεͷ࠶ੜੑ΁ͷରԠͰ͖ͳ͍͕ɻ

Slide 7

Slide 7 text

ফඅܕϓϩμΫτͷ৔߹ͷରԠ߲໨ w ॳظԽͱ઀ଓ w ΞΠςϜͷ৘ใΛऔಘ͢Δ w ߪೖ͢Δ w ߪೖͷ৘ใΛऔಘ͢Δ w ফඅ͢Δ

Slide 8

Slide 8 text

αϯϓϧ5SJWJBM%SJWF,PUMJOͷΫϥεߏ੒ (BNF'SBHNFOU #JMMJOH7JFX.PEFM #JMMJOH3FQPTJUPSZ ࠷৽ͷαϯϓϧ͸,PUMJO'MPXΛ࢖ͬͨ#JMMJOH%BUB4PVSDFΫϥεʹ՝ۚϩδοΫΛ࣋ͬͯߦ͍ͬͯ·͢ɻ

Slide 9

Slide 9 text

ॳظԽͱ઀ଓ 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) } } อཹதͷऔҾΛ༗ޮʹ͢Δ

Slide 10

Slide 10 text

ΞΠςϜͷ৘ใΛऔಘ͢Δ ঎඼ͷ4,6ͷϦετ ఆظߪೖͰͳ͍ fun querySkuDetailsAsync(skuList: List) { 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 -> { // Τϥʔ } } } }

Slide 11

Slide 11 text

ߪೖ͢Δ 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?) { 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): Boolean { purchases.forEach { if (it.getPurchaseState() != Purchase.PurchaseState.PURCHASED) { return true } } return false }

Slide 12

Slide 12 text

ߪೖͷ৘ใΛऔಘ͢Δ fun queryPurchasesAsync() = coroutineScope.launch { val skuType: String = BillingClient.SkuType.INAPP val purchasesResult = billingClient.queryPurchases(skuType) val validPurchases = mutableListOf() purchasesResult?.purchasesList?.let { it.forEach { purchase -> if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { validPurchases.add(purchase) } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) { // อཹதͷऔҾ } } } // validPurchases : ະফඅʢফඅࡁΈʣϦετऔಘ }

Slide 13

Slide 13 text

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/ {packageName}/purchases/products/{productId}/tokens/{token}:acknowledge { "developerPayload": string } .FUIPEQVSDIBTFTQSPEVDUTBDLOPXMFEHF IUUQTEFWFMPQFSTHPPHMFDPNBOESPJEQVCMJTIFSBQJSFGSFTUWQVSDIBTFTQSPEVDUTBDLOPXMFEHF IMFO 3FTQPOTFCPEZ *GTVDDFTTGVM UIFSFTQPOTFCPEZJTFNQUZ

Slide 14

Slide 14 text

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 -> { /* ͦͷଞΤϥʔ */ } } } } ফඅ͢Δ

Slide 15

Slide 15 text

wߪೖͷߋ৽৘ใʹ͸ɺߪೖࡁΈͱอཹதͷ঎඼͕ଘࡏ͢Δɻ wߪೖॲཧͷظݶ͕ઃ͚ΒΕͨɻ w೔ɻςετ؀ڥͰ͸෼ͩͬͨɻ wඇফඅܕϓϩμΫτͰ΋ɺظݶ಺ʹ #JMMJOH$MJFOUBDLOPXMFEHF1VSDIBTF ͕ݺ͹Εͳ͍ͱฦۚͱ ͳΔɻ w(PPHMF1MBZ%FWFMPQFS"1*Λ࢖ͬͨߪೖͷεςʔλεͷ֬ೝ͕ ॏཁͱͳΔɻ