Upgrade to Pro — share decks privately, control downloads, hide ads and more …

詳解定期購入

ymnder
February 07, 2019

 詳解定期購入

Deep dive into subscriptions
DroidKaigi2019, Room2 - 2019/02/07 16:00-16:50
https://droidkaigi.jp/2019/timetable/70670

ymnder

February 07, 2019
Tweet

More Decks by ymnder

Other Decks in Programming

Transcript

  1. Deep dive into subscriptions
    DroidKaigi2019, Room2 - 2019/02/07 16:00-16:50

    View Slide

  2. whoami
    twitter:@ymnd, github:@ymnder
    Application Engineer
    Android
    Android
    https://s.nikkei.com/s_android
    Day2: Room 4 16:50-17:20
    WebView+ViewGroup AOSP
    by ogapants
    2

    View Slide

  3. Today s menu
    3

    View Slide

  4. RealTime Developer Noti cation
    iOS IAP
    4

    View Slide

  5. Caution

    View Slide

  6. Caution
    https://developer.android.com/google/play/billing/
    billing_best_practices

    View Slide

  7. View Slide

  8. 8

    View Slide

  9. 9
    αϒεΫϦϓγϣϯ͸ʮߪೖ͍ͯ͠Δظؒ಺͚ͩʯ
    ౰֘αʔϏεΛ࢖͑ΔΑ͏ʹͳΔ࢓૊Έ

    View Slide

  10. 10
    ՝ۚ ՝ۚ ՝ۚ …
    ղ໿
    ࢧ෷͍ෆঝೝ

    View Slide

  11. or
    11

    View Slide

  12. 12

    View Slide

  13. 13

    View Slide

  14. PlayStore
    14

    View Slide

  15. 15

    View Slide

  16. 16

    View Slide

  17. View Slide

  18. ID ID
    ID
    18

    View Slide

  19. GooglePlay
    ID
    ID
    ID
    19

    View Slide

  20. GooglePlay 30%
    15%
    ref: https://support.google.com/googleplay/android-developer/
    answer/112622?hl=ja
    KPI
    KPI
    20

    View Slide

  21. GooglePlay
    21

    View Slide

  22. PlayStore Web
    PlayStore
    22

    View Slide

  23. Google Play
    23

    View Slide

  24. 20 30
    ID
    Play
    24

    View Slide

  25. PlayStore
    25

    View Slide

  26. View Slide

  27. View Slide

  28. Google Play Billing Library
    GooglePlayStoreApp
    StoreApp
    Android 署
    API wrap
    28

    View Slide

  29. Google Play Billing Library
    https://developer.android.com/reference/com/android/
    billingclient/classes
    v1.1
    v1.2 released (2018-10-18)
    29

    View Slide

  30. app/build.gradle
    30
    dependencies {
    implementation “com.android.billingclient:billing:1.1”
    }

    View Slide

  31. BillingClient
    BillingClient
    31
    var client = BillingClient.newBuilder(context)
    .setListener(object : PurchasesUpdatedListener {
    override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) {
    // call if purchase has done
    }
    })
    .build()

    View Slide

  32. Service
    startConnection
    32
    client.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(@BillingClient.BillingResponse billingResponseCode: Int) {
    if (billingResponseCode == BillingClient.BillingResponse.OK) {
    // The billing client is ready. You can query purchases here.
    }
    }
    override fun onBillingServiceDisconnected() {
    // Try to restart the connection on the next request to
    // Google Play by calling the startConnection() method.
    }
    })

    View Slide

  33. launchBillingFlow
    PurchasesUpdatedListener 踏
    33
    val flowParams = BillingFlowParams.newBuilder()
    .setSku("skuId")
    .setType(BillingClient.SkuType.SUBS) // SkuType.SUB for subscription
    .build()
    val responseCode = client.launchBillingFlow(activity, flowParams)

    View Slide

  34. View Slide

  35. BillingClient / BillingClientImpl
    BillingClient
    PlayStore
    35
    public static final class Builder {
    private final Context mContext;
    private PurchasesUpdatedListener mListener;
    private Builder(Context context) {
    mContext = context;
    }
    @UiThread
    public Builder setListener(PurchasesUpdatedListener listener) {
    mListener = listener;
    return this;
    }
    @UiThread
    public BillingClient build() {
    if (mContext == null) {
    throw new IllegalArgumentException("Please provide a valid Context.");
    }
    if (mListener == null) {
    throw new IllegalArgumentException(/…/);
    }
    return new BillingClientImpl(mContext, mListener);
    }
    }

    View Slide

  36. BillingClient / BillingClientImpl
    PurchasesUpdatedListener

    Client
    36
    @UiThread
    public BillingClient build() {
    if (mContext == null) {
    throw new IllegalArgumentException("Please provide a valid Context.");
    }
    if (mListener == null) {
    throw new IllegalArgumentException(
    "Please provide a valid listener for" + " purchases updates.");
    }
    return new BillingClientImpl(mContext, mListener);
    }

    View Slide

  37. PurchasesUpdatedListener
    purchases null
    37
    public interface PurchasesUpdatedListener {
    void onPurchasesUpdated(@BillingResponse int responseCode, @Nullable List purchases);
    }

    View Slide

  38. BillingClient Service
    start / ready / end
    start InAppBillingService
    isReady start
    end Service unbind null
    end Client
    38

    View Slide

  39. 39
    public @interface ClientState {
    /** This client was not yet connected to billing service or was already disconnected from it. */
    int DISCONNECTED = 0;
    /** This client is currently in process of connecting to billing service. */
    int CONNECTING = 1;
    /** This client is currently connected to billing service. */
    int CONNECTED = 2;
    /** This client was already closed and shouldn't be used again. */
    int CLOSED = 3;
    }

    View Slide

  40. isReady
    startConnection
    endConnection
    40
    @UiThread
    public abstract boolean isReady();
    @UiThread
    public abstract void startConnection(@NonNull final BillingClientStateListener listener);
    @UiThread
    public abstract void endConnection();

    View Slide

  41. isReady
    41
    public boolean isReady() {
    return mClientState == ClientState.CONNECTED && mService != null && mServiceConnection != null;
    }

    View Slide

  42. startConnection
    42
    public void startConnection(@NonNull BillingClientStateListener listener) {
    if (isReady()) {
    listener.onBillingSetupFinished(BillingResponse.OK);
    return;
    }
    if (mClientState == ClientState.CONNECTING) {
    listener.onBillingSetupFinished(BillingResponse.DEVELOPER_ERROR);
    return;
    }
    if (mClientState == ClientState.CLOSED) {
    listener.onBillingSetupFinished(BillingResponse.DEVELOPER_ERROR);
    return;
    }
    mClientState = ClientState.CONNECTING;
    //… if connected, set ClientState.CONNECTED in BillingServiceConnection
    mClientState = ClientState.DISCONNECTED;
    BillingHelper.logVerbose(TAG, "Billing service unavailable on device.");
    listener.onBillingSetupFinished(BillingResponse.BILLING_UNAVAILABLE);
    }

    View Slide

  43. startConnection / onServiceConnected
    IABv6
    43

    View Slide

  44. endConnection
    44
    public void endConnection() {
    try {
    LocalBroadcastManager.getInstance(mApplicationContext)
    .unregisterReceiver(onPurchaseFinishedReceiver);
    mBroadcastManager.destroy();
    if (mServiceConnection != null && mService != null) {
    BillingHelper.logVerbose(TAG, "Unbinding from service.");
    mApplicationContext.unbindService(mServiceConnection);
    mServiceConnection = null;
    }
    mService = null;
    if (mExecutorService != null) {
    mExecutorService.shutdownNow();
    mExecutorService = null;
    }
    } catch (Exception ex) {
    BillingHelper.logWarn(TAG, "There was an exception while ending connection: " + ex);
    } finally {
    mClientState = ClientState.CLOSED;
    }
    }

    View Slide

  45. endConnection
    45
    public void endConnection() {
    try {
    LocalBroadcastManager.getInstance(mApplicationContext)
    .unregisterReceiver(onPurchaseFinishedReceiver);
    mBroadcastManager.destroy();
    if (mServiceConnection != null && mService != null) {
    BillingHelper.logVerbose(TAG, "Unbinding from service.");
    mApplicationContext.unbindService(mServiceConnection);
    mServiceConnection = null;
    }
    mService = null;
    if (mExecutorService != null) {
    mExecutorService.shutdownNow();
    mExecutorService = null;
    }
    } catch (Exception ex) {
    BillingHelper.logWarn(TAG, "There was an exception while ending connection: " + ex);
    } finally {
    mClientState = ClientState.CLOSED;
    }
    }

    View Slide

  46. endConnection
    Activity Client onDestroy
    Application
    I/O 2018
    ref: https://github.com/googlesamples/android-play-billing/
    blob/master/TrivialDriveKotlin/app/src/main/java/com/
    kotlin/trivialdrive/billingrepo/BillingRepository.kt#L210
    46

    View Slide

  47. isFeatureSupported
    PlayStore
    47
    public @BillingResponse int isFeatureSupported(@FeatureType String feature) {
    if (!isReady()) {
    return BillingResponse.SERVICE_DISCONNECTED;
    }
    switch (feature) {
    case FeatureType.SUBSCRIPTIONS:
    return mSubscriptionsSupported ? BillingResponse.OK : BillingResponse.FEATURE_NOT_SUPPORTED;
    case FeatureType.SUBSCRIPTIONS_UPDATE:
    return mSubscriptionUpdateSupported ? BillingResponse.OK: BillingResponse.FEATURE_NOT_SUPPORTED;
    case FeatureType.IN_APP_ITEMS_ON_VR:
    return isBillingSupportedOnVr(SkuType.INAPP);
    case FeatureType.SUBSCRIPTIONS_ON_VR:
    return isBillingSupportedOnVr(SkuType.SUBS);
    default:
    return BillingResponse.DEVELOPER_ERROR;
    }
    }

    View Slide

  48. launchBillingFlow
    48
    public int launchBillingFlow(Activity activity, BillingFlowParams params) {
    if (!isReady()) {
    return broadcastFailureAndReturnBillingResponse(BillingResponse.SERVICE_DISCONNECTED);
    }
    @SkuType String skuType = params.getSkuType();
    String newSku = params.getSku();
    if (newSku == null) {
    BillingHelper.logWarn(TAG, "Please fix the input params. SKU can't be null.");
    return broadcastFailureAndReturnBillingResponse(BillingResponse.DEVELOPER_ERROR);
    }
    try {
    //…
    Intent intent = new Intent(activity, ProxyBillingActivity.class);
    PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
    intent.putExtra(RESPONSE_BUY_INTENT, pendingIntent);
    activity.startActivity(intent);
    } catch (RemoteException e) {
    //…
    }
    return BillingResponse.OK;
    }

    View Slide

  49. launchBillingFlow
    49
    public int launchBillingFlow(Activity activity, BillingFlowParams params) {
    if (!isReady()) {
    return broadcastFailureAndReturnBillingResponse(BillingResponse.SERVICE_DISCONNECTED);
    }
    @SkuType String skuType = params.getSkuType();
    String newSku = params.getSku();
    if (newSku == null) {
    BillingHelper.logWarn(TAG, "Please fix the input params. SKU can't be null.");
    return broadcastFailureAndReturnBillingResponse(BillingResponse.DEVELOPER_ERROR);
    }
    try {
    //…
    Intent intent = new Intent(activity, ProxyBillingActivity.class);
    PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
    intent.putExtra(RESPONSE_BUY_INTENT, pendingIntent);
    activity.startActivity(intent);
    } catch (RemoteException e) {
    //…
    }
    return BillingResponse.OK;
    }

    View Slide

  50. BillingFlowParams
    ID Type
    50
    BillingFlowParams.newBuilder()
    .setSku("skuId")
    .setType(BillingClient.SkuType.SUBS)
    .build()

    View Slide

  51. launchBillingFlow
    Activity BillingFlowParams
    Activity
    Client 踏
    51

    View Slide

  52. launchBillingFlow
    52
    public int launchBillingFlow(Activity activity, BillingFlowParams params) {
    if (!isReady()) {
    return broadcastFailureAndReturnBillingResponse(BillingResponse.SERVICE_DISCONNECTED);
    }
    @SkuType String skuType = params.getSkuType();
    String newSku = params.getSku();
    if (newSku == null) {
    BillingHelper.logWarn(TAG, "Please fix the input params. SKU can't be null.");
    return broadcastFailureAndReturnBillingResponse(BillingResponse.DEVELOPER_ERROR);
    }
    try {
    //…
    Intent intent = new Intent(activity, ProxyBillingActivity.class);
    PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
    intent.putExtra(RESPONSE_BUY_INTENT, pendingIntent);
    activity.startActivity(intent);
    } catch (RemoteException e) {
    //…
    }
    return BillingResponse.OK;
    }

    View Slide

  53. launchBillingFlow
    Activity BillingFlowParams
    Activity
    Client 踏
    53

    View Slide

  54. launchBillingFlow
    Activity Params
    54

    View Slide

  55. queryPurchaseHistoryAsync
    skuId
    55

    View Slide

  56. queryPurchaseHistoryAsync
    56
    public void queryPurchaseHistoryAsync(
    final @SkuType String skuType, final PurchaseHistoryResponseListener listener) {
    if (!isReady()) {
    listener.onPurchaseHistoryResponse(
    BillingResponse.SERVICE_DISCONNECTED, /* purchasesList */ null);
    return;
    }
    executeAsync(
    new Runnable() {
    @Override
    public void run() {
    final PurchasesResult result = queryPurchasesInternal(skuType, /* queryHistory */ true);
    // Post the result to main thread
    postToUiThread(
    new Runnable() {
    @Override
    public void run() {
    listener.onPurchaseHistoryResponse(
    result.getResponseCode(), result.getPurchasesList());
    }
    });
    }
    });
    }

    View Slide

  57. queryPurchaseHistoryAsync
    57
    public void queryPurchaseHistoryAsync(
    final @SkuType String skuType, final PurchaseHistoryResponseListener listener) {
    if (!isReady()) {
    listener.onPurchaseHistoryResponse(
    BillingResponse.SERVICE_DISCONNECTED, /* purchasesList */ null);
    return;
    }
    executeAsync(
    new Runnable() {
    @Override
    public void run() {
    final PurchasesResult result = queryPurchasesInternal(skuType, /* queryHistory */ true);
    // Post the result to main thread
    postToUiThread(
    new Runnable() {
    @Override
    public void run() {
    listener.onPurchaseHistoryResponse(
    result.getResponseCode(), result.getPurchasesList());
    }
    });
    }
    });
    }

    View Slide

  58. queryPurchaseHistoryAsync
    API
    API
    58

    View Slide

  59. queryPurchases
    Play Store App Cache
    59

    View Slide

  60. queryPurchases
    60
    public PurchasesResult queryPurchases(@SkuType String skuType) {
    if (!isReady()) {
    return new PurchasesResult(BillingResponse.SERVICE_DISCONNECTED, /* purchasesList */ null);
    }
    // Checking for the mandatory argument
    if (TextUtils.isEmpty(skuType)) {
    BillingHelper.logWarn(TAG, "Please provide a valid SKU type.");
    return new PurchasesResult(BillingResponse.DEVELOPER_ERROR, /* purchasesList */ null);
    }
    return queryPurchasesInternal(skuType, false /* queryHistory */);
    }

    View Slide

  61. PlayStore
    61

    View Slide

  62. launchBillingFlow
    onPurchasesUpdated(int responseCode, List
    purchases)
    queryPurchaseHistoryAsync
    onPurchaseHistoryResponse(int responseCode,
    List purchasesList)
    queryPurchases
    Purchase.PurchasesResult
    62

    View Slide

  63. 63

    View Slide

  64. autoRenewing
    orderId ID
    packageName
    productId ID
    purchaseState
    purchaseTime
    purchaseToken
    https://developer.android.com/google/play/billing/billing_reference?
    hl=ja#getBuyIntent
    64

    View Slide

  65. autoRenewing
    orderId ID
    packageName
    productId ID
    purchaseState
    purchaseTime
    purchaseToken
    https://developer.android.com/google/play/billing/billing_reference?
    hl=ja#getBuyIntent
    65

    View Slide

  66. launchBillingFlow
    66
    {
    "autoRenewing": true,
    "orderId": "GPA.0000-1111-2222-33444",
    "packageName": "com.nikkei.newspaper",
    "productId": "skuId",
    "purchaseState": 0,
    "purchaseTime": 1549417790186,
    "purchaseToken": “…”
    }

    View Slide

  67. queryPurchaseHistoryAsync
    67
    {
    “productId":"skuId",
    “purchaseToken”:”…",
    “purchaseTime":1549417790186,
    “developerPayload”:null
    }

    View Slide

  68. queryPurchases
    68
    {
    "autoRenewing": true,
    "orderId": "GPA.0000-1111-2222-33444",
    "packageName": "com.nikkei.newspaper",
    "productId": "skuId",
    "purchaseState": 0,
    "purchaseTime": 1549417790186,
    "purchaseToken": “…”
    }

    View Slide

  69. queryPurchases
    69
    {
    "autoRenewing": false,
    "orderId": "GPA.0000-1111-2222-33444",
    "packageName": "com.nikkei.newspaper",
    "productId": “skuId",
    "purchaseState": 0,
    "purchaseTime": 1549417790186,
    "purchaseToken": “…”
    }

    View Slide

  70. queryPurchaseHistoryAsync
    launchBillingFlow queryPurchases
    70

    View Slide

  71. queryPurchaseHistoryAsync
    queryPurchases
    autoRenewing
    71

    View Slide

  72. BillingResponse
    72
    public @interface BillingResponse {
    int FEATURE_NOT_SUPPORTED = -2;
    int SERVICE_DISCONNECTED = -1;
    /** Success */
    int OK = 0;
    /** User pressed back or canceled a dialog */
    int USER_CANCELED = 1;
    /** Network connection is down */
    int SERVICE_UNAVAILABLE = 2;
    /** Billing API version is not supported for the type requested */
    int BILLING_UNAVAILABLE = 3;
    /** Requested product is not available for purchase */
    int ITEM_UNAVAILABLE = 4;
    int DEVELOPER_ERROR = 5;
    /** Fatal error during the API action */
    int ERROR = 6;
    /** Failure to purchase since item is already owned */
    int ITEM_ALREADY_OWNED = 7;
    /** Failure to consume since item is not owned */
    int ITEM_NOT_OWNED = 8;
    }

    View Slide

  73. BillingResponse
    startConnection
    73
    return when (resultCode) {
    BillingClient.BillingResponse.FEATURE_NOT_SUPPORTED -> "FEATURE_NOT_SUPPORTED"
    BillingClient.BillingResponse.SERVICE_DISCONNECTED -> "SERVICE_DISCONNECTED"
    BillingClient.BillingResponse.OK -> "OK"
    BillingClient.BillingResponse.USER_CANCELED -> "USER_CANCELED"
    BillingClient.BillingResponse.SERVICE_UNAVAILABLE -> "SERVICE_UNAVAILABLE"
    BillingClient.BillingResponse.BILLING_UNAVAILABLE -> "BILLING_UNAVAILABLE"
    BillingClient.BillingResponse.ITEM_UNAVAILABLE -> "ITEM_UNAVAILABLE"
    BillingClient.BillingResponse.DEVELOPER_ERROR -> "DEVELOPER_ERROR"
    BillingClient.BillingResponse.ERROR -> "ERROR"
    BillingClient.BillingResponse.ITEM_ALREADY_OWNED -> "ITEM_ALREADY_OWNED"
    BillingClient.BillingResponse.ITEM_NOT_OWNED -> "ITEM_NOT_OWNED"
    else -> "UNDEFINED:$resultCode"
    }

    View Slide

  74. querySkuDetailsAsync
    v1.2
    launchPriceChangeCon rmationFlow
    loadRewardedSku
    setChildDirected
    74

    View Slide

  75. View Slide

  76. UI
    https://note.mu/telq/n/n836e139e0a6b
    76

    View Slide

  77. 77

    View Slide

  78. ID
    78

    View Slide

  79. 79

    View Slide

  80. View Slide

  81. ref: https://developer.android.com/google/play/billing/
    billing_admin.html?hl=ja
    81

    View Slide

  82. GooglePlayConsole
    >
    82

    View Slide

  83. 83

    View Slide

  84. ID skuId productId, ID
    84

    View Slide

  85. >
    85

    View Slide

  86. com.android.vending.BILLING
    implementation com.android.billingclient:billing:1.1'
    86

    View Slide

  87. 87

    View Slide

  88. TestCard
    TestCard
    Google Play
    https://support.google.com/googleplay/android-developer/answer/6062777?
    hl=ja
    88

    View Slide

  89. > 

    or URL
    89

    View Slide

  90. > >
    Gmail
    90

    View Slide

  91. 4,200/5min
    GPay TestCard
    91

    View Slide

  92. / 1 / 5min, 1 / 5min

    https://developer.android.com/google/play/billing/
    billing_testing#testing-renewals
    92

    View Slide

  93. PlayStore
    PlayStore
    https://github.com/googlesamples/android-play-billing/issues/2
    PlayStore
    93

    View Slide

  94. PlayStore
    94

    View Slide

  95. ID
    ID
    PlayStoreApp App
    Console
    95

    View Slide

  96. applicationID ID
    avor
    96

    View Slide

  97. tips
    ref: https://techbookfest.org/event/tbf04/circle/14710011
    store
    ID
    97

    View Slide

  98. tips
    98

    View Slide

  99. View Slide

  100. skuId
    PlayStore API
    skuId
    querySkuDetailsAsync
    100

    View Slide

  101. BillingActivity
    Activity 踏

    Activity
    BillingClient Activity
    Activity issue
    ref: https://github.com/googlesamples/android-play-billing/
    issues/95
    101

    View Slide

  102. RxJava Rx
    Kotlin
    ref: https://github.com/googlesamples/android-play-billing/
    blob/master/TrivialDriveKotlin/app/src/main/java/com/
    kotlin/trivialdrive/billingrepo/BillingRepository.kt#L210
    102

    View Slide

  103. 103
    SFNPUF MPDBM
    SFQPTJUPSZ
    VTFDBTF
    QSFTFOUFS
    WJFX

    View Slide

  104. Remote Local Repository
    interface RxJava Single Completable
    Remote BillingClient
    BillingLibrary Manager
    Listener
    104

    View Slide

  105. launchBilling ow
    launchBilling ow
    BillingClient Listener
    Callback
    105

    View Slide

  106. launchBilling ow
    launchBilling ow
    PurchasesUpdatedListener 踏
    RxJava PublishSubject
    106

    View Slide

  107. launchBilling ow
    107

    View Slide

  108. launchBilling ow
    PublishSubject
    Subject Subscriber Observable
    Listener
    subscribe onNext
    instance callback PublishSubject callback
    onNext 踏 Rx
    Remote Single Wrap
    Repository
    108

    View Slide

  109. BillingManager
    109
    interface PlayBillingManager {
    fun observePurchasesUpdated(): PublishSubject
    fun isReady(): Boolean
    fun startConnection(listener: BillingClientStateListener)
    fun launchBillingFlow(activity: Activity, params: BillingFlowParams): Int
    fun queryPurchases(): Purchase.PurchasesResult
    fun endConnection()
    fun queryPurchaseHistoryAsync(skuId: String): Single
    }

    View Slide

  110. BillingApiManager
    110
    @Singleton
    class PlayBillingManagerImpl @Inject constructor(
    private val context: Context
    ) : PlayBillingManager, PurchasesUpdatedListener {
    private val publishSubject = PublishSubject.create()
    override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) {
    if (responseCode == BillingClient.BillingResponse.OK) {
    publishSubject.onNext(PurchasesUpdatedResponse(responseCode, purchases))
    } else {
    publishSubject.onNext(PurchasesUpdatedResponse(responseCode))
    }
    }
    override fun observePurchasesUpdated(): PublishSubject {
    return publishSubject
    }
    override fun launchBillingFlow(activity: Activity, params: BillingFlowParams): Int {
    return billingClient.launchBillingFlow(activity, params)
    }
    }

    View Slide

  111. BillingApiManager
    111
    @Singleton
    class PlayBillingManagerImpl @Inject constructor(
    private val context: Context
    ) : PlayBillingManager, PurchasesUpdatedListener {
    private val publishSubject = PublishSubject.create()
    override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) {
    if (responseCode == BillingClient.BillingResponse.OK) {
    publishSubject.onNext(PurchasesUpdatedResponse(responseCode, purchases))
    } else {
    publishSubject.onNext(PurchasesUpdatedResponse(responseCode))
    }
    }
    override fun observePurchasesUpdated(): PublishSubject {
    return publishSubject
    }
    override fun launchBillingFlow(activity: Activity, params: BillingFlowParams): Int {
    return billingClient.launchBillingFlow(activity, params)
    }
    }

    View Slide

  112. Remote
    112
    @Singleton
    class RemotePlayBillingImpl @Inject constructor(
    private val billingClient: PlayBillingManager
    ) : RemotePlayBilling {
    override fun launchBillingFlow(activity: Activity, skuId: String): Single {
    return Single.create {
    val flowParams = BillingFlowParams.newBuilder()
    .setSku(skuId)
    .setType(TYPE_SUBSCRIPTION)
    .build()
    val responseCode = billingClient.launchBillingFlow(activity, flowParams)
    if (responseCode == BillingClient.BillingResponse.OK) {
    it.onSuccess(PurchasePaperResponse.PurchaseSuccess(responseCode))
    } else {
    it.onError(PurchaseException(responseCode, "ߪಡΤϥʔ"))
    }
    }
    }
    override fun observePurchasesUpdated(): Observable {
    return billingClient.observePurchasesUpdated()
    }
    }

    View Slide

  113. Remote
    113
    @Singleton
    class RemotePlayBillingImpl @Inject constructor(
    private val billingClient: PlayBillingManager
    ) : RemotePlayBilling {
    override fun launchBillingFlow(activity: Activity, skuId: String): Single {
    return Single.create {
    val flowParams = BillingFlowParams.newBuilder()
    .setSku(skuId)
    .setType(TYPE_SUBSCRIPTION)
    .build()
    val responseCode = billingClient.launchBillingFlow(activity, flowParams)
    if (responseCode == BillingClient.BillingResponse.OK) {
    it.onSuccess(PurchasePaperResponse.PurchaseSuccess(responseCode))
    } else {
    it.onError(PurchaseException(responseCode, "ߪಡΤϥʔ"))
    }
    }
    }
    override fun observePurchasesUpdated(): Observable {
    return billingClient.observePurchasesUpdated()
    }
    }

    View Slide

  114. startConnection
    launchBilling ow
    PurchasesUpdatedListener
    114

    View Slide

  115. Repository
    115
    class PlayBillingRepositoryImpl @Inject constructor(
    private val remote: RemotePlayBilling
    ) : PlayBillingRepository {
    override fun startSubscription(activity: Activity, skuId: String): Observable {
    return remote.connect()
    .flatMapObservable {
    remote.launchBillingFlow(activity, skuId)
    .flatMapObservable {
    remote.observePurchasesUpdated()
    .flatMap {
    if (it.result != BillingClient.BillingResponse.OK) {
    return@flatMap Observable.error(…)
    }
    return@flatMap Observable.just(it)
    }
    //…
    }
    }
    }
    }

    View Slide

  116. Repository
    116
    class PlayBillingRepositoryImpl @Inject constructor(
    private val remote: RemotePlayBilling
    ) : PlayBillingRepository {
    override fun startSubscription(activity: Activity, skuId: String): Observable {
    return remote.connect()
    .flatMapObservable {
    remote.launchBillingFlow(activity, skuId)
    .flatMapObservable {
    remote.observePurchasesUpdated()
    .flatMap {
    if (it.result != BillingClient.BillingResponse.OK) {
    return@flatMap Observable.error(…)
    }
    return@flatMap Observable.just(it)
    }
    //…
    }
    }
    }
    }

    View Slide

  117. RemoteTest
    117
    @Before
    fun setUp() {
    manager = mock(PlayBillingManager::class.java)
    remote = RemotePlayBillingImpl(manager)
    }

    @Test
    fun connect_isReady() {
    `when`(manager.isReady()).thenReturn(true)
    val response = remote.connect()
    .test()
    .await()
    .values()[0]
    assertThat(response).isNotNull()
    assertThat(response.result).isEqualTo(BillingClient.BillingResponse.OK)
    }

    View Slide

  118. RemoteTest
    118
    @Test
    fun observePurchasesUpdated_error() {
    val subject = spy(PublishSubject.create())
    `when`(remote.observePurchasesUpdated()).thenReturn(subject)
    val latch = CountDownLatch(2)
    var responseCode = 0
    remote.observePurchasesUpdated()
    .subscribe({
    responseCode += 1
    latch.countDown()
    }, {
    latch.countDown()
    })
    subject.onNext(PurchasesUpdatedResponse(0, null))
    subject.onError(PurchaseException(BillingClient.BillingResponse.ERROR, "ΤϥʔͰ͢ʂʂ"))
    subject.onNext(PurchasesUpdatedResponse(1, null))
    subject.onError(PurchaseException(BillingClient.BillingResponse.ERROR, "ΤϥʔͰ͢ʂʂ"))
    latch.await()
    assertThat(responseCode).isEqualTo(1)
    verify(subject, times(2)).onNext(any())
    verify(subject, times(2)).onError(any())
    }

    View Slide


  119. 119

    View Slide

  120. View Slide

  121. PlayStoreConsole
    121

    View Slide

  122. 122

    View Slide

  123. >
    123

    View Slide

  124. >
    124

    View Slide

  125. > >
    125

    View Slide

  126. 126

    View Slide

  127. 127

    View Slide

  128. 30
    1 2 9:51 2 2 9:51 3 2 9:51
    128

    View Slide

  129. 3
    129

    View Slide

  130. v1.1
    > :
    ID ID
    v1.2
    launchPriceChangeCon rmationFlow
    130

    View Slide

  131. https://github.com/googlesamples/android-play-billing
    https://developer.android.com/google/play/billing/billing_overview
    Library
    PlayConsole
    131

    View Slide

  132. Hello Subscription :)

    View Slide

  133. View Slide

  134. PlayStore
    https://play.google.com/store/account/subscriptions?
    sku=XXX&package=YYY
    134

    View Slide

  135. PlayStore app music video
    135

    View Slide

  136. Google I/O
    https://www.youtube.com/watch?v=x1AYelepG6o
    https://www.youtube.com/watch?v=oib_gHJA_-
    https://www.youtube.com/watch?v=6IT689K3kOo
    https://developer.android.com/google/play/billing/
    https://support.google.com/googleplay/android-developer/answer/140504?hl=ja
    https://github.com/googlesamples/android-play-billing
    RxJava
    https://techlife.cookpad.com/entry/2018/03/14/090000
    https://github.com/bu erapp/ReactivePlayBilling
    https://github.com/vberezkin/billing-android
    136

    View Slide

  137. Account Hold
    https://techbookfest.org/event/tbf04/circle/16610003
    v1.0
    https://medium.com/exploring-android/exploring-the-play-
    billing-library-for-android-55321f282929
    v1.1
    https://speakerdeck.com/ymnder/whats-new-in-google-play-
    billing
    137

    View Slide