Android Tutorial Perform GooglePlayBilling Subscription Up/Downgrade

Discussion in 'Tutorials & Examples' started by fredo, Nov 6, 2019.

  1. fredo

    fredo Well-Known Member Licensed User

    Based on Erel's solution to the question "Howto GooglePlayBilling Subscription Up/Downgrade?" a test project was created. It should enable interested developers to explore the variants of Google Play In-App subscriptions.

    2019-11-06_14-14-23.png

    Code:
    ' ~~~~~~~~~~~~~~~~~~~~
    ' MANIFEST
    ' ~~~~~~~~~~~~~~~~~~~~
    'This code will be applied to the manifest file during compilation.
    'You do not need to modify it in most cases.
    'See this link for for more information: https://www.b4x.com/forum/showthread.php?p=78136
    AddManifestText(
    <uses-sdk android:minSdkVersion=
    "15" android:targetSdkVersion="28"/>
    <supports-screens android:largeScreens=
    "true"
        android:normalScreens=
    "true"
        android:smallScreens=
    "true"
        android:anyDensity=
    "true"/>)
    SetApplicationAttribute(android:icon, 
    "@drawable/icon")
    SetApplicationAttribute(android:
    label"$LABEL$")
    CreateResourceFromFile(Macro, Themes.DarkTheme)
    'End of default text.
    CreateResourceFromFile(Macro, GooglePlayBilling.GooglePlayBilling)




    ' ~~~~~~~~~~~~~~~~~~~~
    ' MAIN MODULE
    ' ~~~~~~~~~~~~~~~~~~~~
    #Region  Project Attributes
        
    #ApplicationLabel: a B4A Billing Subscriptions Example
        
    #VersionCode: 12
        
    #VersionName: v0.1
        
    'SupportedOrientations possible values: unspecified, landscape or portrait.
        #SupportedOrientations: unspecified
        
    #CanInstallToExternalStorage: False
    #End Region

    ' Testers Playstore-Link --> https://play.google.com/apps/testing/b4a.example.biltest001

    ' Google's howto --> https://developer.android.com/google/play/billing/billing_testing

    ' User-test a Google Play Billing app --> https://developer.android.com/google/play/billing/billing_testing#test-purchases-sandbox

    ' Google Test subscriptions-specific features --> https://developer.android.com/google/play/billing/billing_testing.html#testing-subscriptions
    #Region  Activity Attributes
        
    #FullScreen: true   
        
    #IncludeTitle: false
    #End Region

    Sub Process_Globals
        
    Dim BillingMapOfAvailableProducts As Map
        
    Dim BillingActiveProductId As String = ""
        
    Dim BillingActiveCostLevel As Int = 0
    End Sub
    Sub Globals
        
    Private LabelAskProducts As B4XView
        
    Private BillingProductsClv As CustomListView
        
    Private clvitemBackPanel As B4XView
        
    Private clvitemLabel1 As B4XView
        
    Private clvitemLabel3 As B4XView
        
    Private clvitemLabel4 As B4XView
        
    Private clvitemButton1 As B4XView
        
    Private StateOnline As B4XView
    End Sub
    Sub Activity_Create(FirstTime As Boolean)
        
    Activity.LoadLayout("Layout1")
        
    Activity.Color = Colors.White
        StateOnline.Text = 
    ""
        
        BillingMapOfAvailableProducts = GetBillingSubProducts
        
    End Sub
    Sub Activity_Resume
        
    wait for(BillingAskProductsAndFillClv) complete(nix As Object)
    End Sub
    Sub Activity_Pause (UserClosed As Boolean)
    End Sub

     
    ' -------------------------------------

    private Sub GetBillingSubProducts As Map
        
    ' These are the App's Subsciption products as they are implemented in Google Playstore
        ' Path: GooglePlay console/ Store presence/ In-App products/ Subsriptions
        
        
    ' The items should be ordered from lowest price to highest, so that
        ' an "up/downgrade" option can be implemented
        
        
    Dim MapOfBillingSubscritions As Map
        MapOfBillingSubscritions.Initialize
        
        
    Dim BillingSubProduct As BillingSubProduct
        BillingSubProduct.Initialize
        BillingSubProduct.ProductID = 
    "free"
        BillingSubProduct.OrderId = 
    ""
        BillingSubProduct.Title = 
    "Free Testversion"
        BillingSubProduct.Description = 
    $"Basic functions. ${CRLF}Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa."$
        BillingSubProduct.Price = 
    "0.00"
        BillingSubProduct.CostLevel = 
    0
        BillingSubProduct.FreeTrialPeriod = 
    0
        BillingSubProduct.BillingPeriod = 
    "free"
        BillingSubProduct.DeveloperPayload = 
    ""
        MapOfBillingSubscritions.Put(BillingSubProduct.ProductID, BillingSubProduct)
        
        
    Dim BillingSubProduct As BillingSubProduct
        BillingSubProduct.Initialize
        BillingSubProduct.ProductID = 
    "subsprod1"
        BillingSubProduct.OrderId = 
    ""
        BillingSubProduct.Title = 
    "Prod1 SubWeekly"
        BillingSubProduct.Description = 
    $"Testproduct for SUBS "weekly". ${CRLF}Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa."$
        BillingSubProduct.Price = 
    "0.50"
        BillingSubProduct.CostLevel = 
    1
        BillingSubProduct.FreeTrialPeriod = 
    3
        BillingSubProduct.BillingPeriod = 
    "week"
        BillingSubProduct.DeveloperPayload = 
    ""
        MapOfBillingSubscritions.Put(BillingSubProduct.ProductID, BillingSubProduct)

        
        
    Dim BillingSubProduct As BillingSubProduct
        BillingSubProduct.Initialize
        BillingSubProduct.ProductID = 
    "subsprod2"
        BillingSubProduct.OrderId = 
    ""
        BillingSubProduct.Title = 
    "Prod2 SubMonth"
        BillingSubProduct.Description = 
    $"Testproduct for SUBS "monthly". ${CRLF}Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa."$
        BillingSubProduct.Price = 
    "1.00"
        BillingSubProduct.CostLevel = 
    2
        BillingSubProduct.FreeTrialPeriod = 
    3
        BillingSubProduct.BillingPeriod = 
    "month"
        BillingSubProduct.DeveloperPayload = 
    ""
        MapOfBillingSubscritions.Put(BillingSubProduct.ProductID, BillingSubProduct)
        
        
    Return MapOfBillingSubscritions
    End Sub

    Sub BillingAskProductsAndFillClv As ResumableSub
        
    Log("#-Sub main.AskProducts")
        LabelAskProducts.Text = 
    "Working..."
        
        StateOnline.Text = 
    Chr(0xE2C1' cloud OFF
        
        
    Dim bspNull As BillingSubProduct
        bspNull.Initialize
        BillingActiveCostLevel = 
    0
        
        
    Dim sb As StringBuilder
        sb.Initialize
        
        
    Wait For (Starter.billing.ConnectIfNeeded) Billing_Connected (Result As BillingResult)
        
        
    If Result.IsSuccess Then
            StateOnline.Text = 
    Chr(0xE2C2' cloud ON
            If Starter.billing.SubscriptionsSupported Then
                sb.Append(
    "SUBS supported, ")
            
    Else
                sb.Append(
    "SUBS not supported, ")
            
    End If
            
            
    Wait For (Starter.billing.QueryPurchases("subs")) Billing_PurchasesQueryCompleted (Result As BillingResult, Purchases As List)
            
    If Result.IsSuccess Then
                
    Log("#-")
                
    Log("#-  x106, Purchases.size=" & Purchases.Size)
                
    If Purchases.Size = 0 Then
                    
    Dim bspdefault As BillingSubProduct = BillingMapOfAvailableProducts.Get("free")
                    BillingActiveProductId = bspdefault.ProductID
                    BillingActiveCostLevel = bspdefault.CostLevel
                    Starter.BillingActiveSubsProductTitle = bspdefault.Title
                
    Else
                
                    
    For Each p As Purchase In Purchases
                        
    Log("#-    x134, p.OrderId          = " & p.OrderId)
                        
    Log("#-    x135, p.PurchaseState    = " & p.PurchaseState & " --> " & Starter.BillingPurchState(p.PurchaseState))
                        
    Log("#-    x136, p.PurchaseTime     = " & p.PurchaseTime & $" --> $DateTime{p.PurchaseTime}"$)
                        
    Log("#-    x137, p.Sku              = " & p.Sku )
                        
    Log("#-    x138, p.IsAcknowledged   = " & p.IsAcknowledged)
                        
    Log("#-    x139, p.DeveloperPayload = " & p.DeveloperPayload)
                        
    Log("#-    x140, p.IsAutoRenewing   = " & p.IsAutoRenewing)
                        
                        sb.Append(p.Sku).Append(
    ", ")
                    
                        
    If p.Sku <> Null Then
                            
    Dim bspx As BillingSubProduct = BillingMapOfAvailableProducts.GetDefault(p.Sku, bspNull)
                            
    If bspx.ProductID = p.Sku Then
                                BillingActiveProductId    = p.Sku                       
                                bspx.DeveloperPayload = p.DeveloperPayload
                                BillingMapOfAvailableProducts.put(p.Sku, bspx)
                                BillingActiveCostLevel = bspx.CostLevel
                                Starter.BillingActiveSubsProductTitle = bspx.Title
                            
    End If
                        
    End If
                        
                    
    Next
                
    End If
                LabelAskProducts.Text = sb.ToString
            
    End If
        
    Else
            LabelAskProducts.Text = 
    "Not connected"
            
    Log("#-  x139, not connected")
        
    End If
        
    Log("#-")
        
        FillClv_Products
        
        
    Return Null
    End Sub
     
    private Sub FillClv_Products
        BillingProductsClv.Clear
        
    Dim i As Int = 0
        
    For Each k As String In BillingMapOfAvailableProducts.Keys
            
    Dim bsp As BillingSubProduct = BillingMapOfAvailableProducts.Get(k)
            BillingProductsClv.Add(BillingProductsClv_CreateItem(bsp, i), i)       
            i = i +
    1
        
    Next
    End Sub

    private Sub BillingProductsClv_CreateItem(bsp As BillingSubProduct, Index As Int) As B4XView
        
    'Log($"#-Sub main.BillingProductsClv_CreateItem, Index=${Index}, bsp.ProductID="${bsp.ProductID}"$ )
        Dim PanelHeight As Int = 216dip
        
    Dim p As Panel
        p.Initialize(
    "")
        p.SetLayout(
    00, BillingProductsClv.AsView.Width, PanelHeight)
        p.LoadLayout(
    "billing_clvitem")
        clvitemBackPanel.SetLayoutAnimated(
    08dip0, BillingProductsClv.AsView.Width -16dip, PanelHeight -8dip)
        clvitemLabel1.SetLayoutAnimated(
    000, BillingProductsClv.AsView.Width -16dip40dip)
        clvitemLabel1.Text = bsp.Title
        clvitemLabel3.Text = bsp.Description
        clvitemLabel4.Text = bsp.Price
        clvitemLabel4.SetColorAndBorder(Starter.xui.Color_White, 
    2dip, Starter.xui.Color_LightGray, 5dip)
        clvitemButton1.Tag = bsp.ProductID
        
        
    'Log("#-  x205, BillingActiveProductId=" & BillingActiveProductId & ", BillingActiveCostLevel=" & BillingActiveCostLevel)
        If BillingActiveProductId = bsp.ProductID Then
            clvitemLabel4.Text = 
    "Activated"
            clvitemButton1.Text = 
    "Show my Subscriptions"
        
    Else
            
    If BillingActiveProductId = "free" Then
                clvitemButton1.Text = 
    "BUY"
            
    Else If Index > BillingActiveCostLevel Then
                clvitemButton1.Text = 
    "UPgrade"
            
    else If Index < BillingActiveCostLevel Then
                
                
    If bsp.ProductID = "free" Then
                    clvitemButton1.Text = 
    "Cancel Subscription"
                
    Else
                    clvitemButton1.Text = 
    "DOWNgrade"
                
    End If
                
            
    Else
                clvitemButton1.Text = 
    "BUY"
            
    End If
        
    End If
        
        
    If bsp.ProductID = "free" Then
        
    End If
        
    Return p
    End Sub

    Sub clvitemButton1_Click As ResumableSub
        
    Log("#-")
        
    Dim ButtonX As Button = Sender
        
    Dim bspSelectedByUser As BillingSubProduct = BillingMapOfAvailableProducts.Get(ButtonX.Tag)
        
    Log("#-Sub main.clvitemButton1_Click, bspSelectedByUser.ProductID=" & bspSelectedByUser.ProductID )
        
    If bspSelectedByUser.ProductID = "free" Then
            
    ' Cancel subscription
            
        
    Dim sf As Object = Msgbox2Async($"The subscription cancellation takes place in the Playstore. All important information about the termination date will be displayed there."$"Cancel Subscription" , "OK"""""NullTrue )
        
    Wait For (sf) Msgbox_Result (intIlResult As Int)

            
    StartActivity (Starter.pi.OpenBrowser ("https://play.google.com/store/account/subscriptions") )
        
        
    else If bspSelectedByUser.ProductID = BillingActiveProductId Then
            
    StartActivity (Starter.pi.OpenBrowser ("https://play.google.com/store/account/subscriptions") )
            
        
    Else
            CallSubDelayed2(Me, 
    "BuyProductBySku", bspSelectedByUser)
        
    End If
        
    Return Null
    End Sub

    Sub BuyProductBySku(bsp As BillingSubProduct) As ResumableSub
        
    Dim ProductToBuy As String = bsp.ProductID
        
    Log("#-Sub main.BuyProduct, ProductToBuy=" & ProductToBuy & ", " & $"$DateTime{DateTime.Now}"$)
        LabelAskProducts.Text = 
    $"Buy product "${ProductToBuy}" in progress"$
        
    Log("#-  x239, BillingActiveProductId=" & BillingActiveProductId)
        
        
    Wait For (Starter.billing.ConnectIfNeeded) Billing_Connected (Result As BillingResult)
        
    Log("#-  x241, Result.DebugMessage=" & Result.DebugMessage)
        
    If Result.IsSuccess Then
            
            
    Dim sf As Object = Starter.billing.QuerySkuDetails("subs"Array(ProductToBuy))
            
    Wait For (sf) Billing_SkuQueryCompleted (Result As BillingResult, SkuDetails As List)
            
    If Result.IsSuccess And SkuDetails.Size = 1 Then

                
    Log("#-  x249, BillingActiveProductId=" & BillingActiveProductId)
                
    If BillingActiveProductId <> "free" Then
                    Result = LaunchBillingFlow2(SkuDetails.Get(
    0), BillingActiveProductId)
                
    Else
                    
    'start the billing process. The PurchasesUpdated event will be raised in the starter service
                    Result = Starter.billing.LaunchBillingFlow(SkuDetails.Get(0))
                
    End If
                
    Log("#-  x78, Result.IsSuccess=" & Result.IsSuccess)

                
    If Result.IsSuccess Then
                    LabelAskProducts.Text = 
    $"Product "${ProductToBuy}" bought ok"$
                    
    Return Null
                
    Else
                    LabelAskProducts.Text = 
    $"Failed to buy product "${ProductToBuy}" "$
                
    End If
            
    End If
        
    End If
        
    ToastMessageShow("#-  x74, Error starting billing process"True)
        
    Return Null
    End Sub

    Sub LaunchBillingFlow2(sku As SkuDetails, OldSku As StringAs BillingResult
        
    ' Google --> https://developer.android.com/google/play/billing/billing_subscriptions.html#Allow-upgrade
        ' Erel --> https://www.b4x.com/android/forum/threads/howto-googleplaybilling-subscription-up-downgrade.111090/#post-693109
        '   should be called from the activity
        Dim jo As JavaObject = Starter.billing
        
    Dim BillingClient As JavaObject = jo.GetField("client")
        
    Dim context As JavaObject
        context.InitializeContext
        
    Dim BillingFlowParams As JavaObject
        BillingFlowParams = BillingFlowParams.InitializeStatic(
    "com.android.billingclient.api.BillingFlowParams") _
                   .RunMethodJO(
    "newBuilder"Null).RunMethodJO("setSkuDetails"Array(sku)) _
                   .RunMethodJO(
    "setOldSku"Array(OldSku)).RunMethod("build"Null)
        
    Return BillingClient.RunMethod("launchBillingFlow"Array(context, BillingFlowParams))
    End Sub

    Sub AskPurchState_Click
        CallSubDelayed(Me, 
    "BillingAskProductsAndFillClv")
    End Sub

     
    ' -------------------------------------


    ' ~~~~~~~~~~~~~~~~~~~~
    ' STARTER SERVICE MODULE
    ' ~~~~~~~~~~~~~~~~~~~~
    #Region  Service Attributes
        
    #StartAtBoot: False
        
    #ExcludeFromLibrary: True
    #End Region

    Sub Process_Globals
        
    Type BillingSubProduct(ProductID As String, Title As String, Description As String, Price As String, BillingPeriod As String, FreeTrialPeriod As Int, OrderId As String, CostLevel As Int, DeveloperPayload As String)
        
    Public xui As XUI
        
    Public billing As BillingClient
        
    Public PurchSubsOk As Boolean
        
    ' ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~
        
        
        
    Public const BILLING_KEY As String = "MIIBI..."  ' <-- YOUR "licence key for this application" in GooglePlaystore developer console/ thisapp/ Development tools/ Services & APIs
        
        
        
    ' ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~
        Public const BILLINGPROD1 As String = "subsprod1"
        
    Public const BILLINGPROD2 As String = "subsprod2"
        
    Public BillingPurchState(3As String
        
    Public BillingActiveSubsProductTitle As String = ""
        
    Public pi As PhoneIntents
    End Sub

    Sub Service_Create
        billing.Initialize(
    "billing")
        BillingPurchState(
    0) = "STATE_UNSPECIFIED"
        BillingPurchState(
    1) = "STATE_PURCHASED"
        BillingPurchState(
    2) = "STATE_PENDING"
    End Sub

    ' --- --- --- --- --- --- --- --- --- --- --- --- ---
    Sub billing_PurchasesUpdated (Result As BillingResult, Purchases As List)
        
    Log("#-Sub starter.Sub billing_PurchasesUpdated, Result.IsSuccess=" & Result.IsSuccess)
        
    'This event will be raised when the status of one or more of the purchases has changed.
        'It will usually happen as a result of calling LaunchBillingFlow however it can be called in other cases as well.
        If Result.IsSuccess Then
            
    For Each p As Purchase In Purchases
                
    Log("#-    x39, p.Sku = " & p.Sku)
                
    If p.Sku.StartsWith("subsprod"Then
                    HandleSubscriptionPurchase(p)
                
    Else
                    
    Log("#-    x43, Unexpected product...")
                
    End If
            
    Next
        
    End If
    End Sub

    Private Sub HandleSubscriptionPurchase (p As PurchaseAs ResumableSub
        
    Log("#-Sub starter.Sub HandleSubscriptionPurchase, p.sku=" & p.Sku)
        
    If p.PurchaseState <> p.STATE_PURCHASED Then Return Null
        
        
    ' --> https://developer.android.com/google/play/billing/billing_testing.html#testing-renewals
        
        
    'Verify the purchase signature.
        'This cannot be done with the test id.
        If Not(p.Sku.StartsWith("subsprod")) And billing.VerifyPurchase(p, BILLING_KEY) = False Then  
            
    Log("#-  x55, Invalid purchase")
            
    Return Null
        
    End If
        
    If p.IsAcknowledged = False Then
            
    'we either acknowledge the product or consume it.
            Dim Intval As Int = 3 ' Testdays
            Select Case p.Sku
                
    Case "subsprod1" ' Weekly subscription
                    Intval= 7
                
    Case "subsprod2" ' Monthly subscription
                    Intval= 30
            
    End Select
            
    Dim SubsEndTime As Long = p.PurchaseTime + (DateTime.TicksPerDay *Intval)
            
    Dim mypayload As String = "mp~" & p.Sku & "~" & p.PurchaseTime & "~" & Intval & "~" & SubsEndTime
            
    Wait For (billing.AcknowledgePurchase(p.PurchaseToken, mypayload)) Billing_AcknowledgeCompleted (Result As BillingResult)
            
    Log("#-  x61, Acknowledged: " & Result.IsSuccess & ", p.OrderId=" & p.OrderId)
        
    End If
        PurchSubsOk = 
    True
        
    Log("#-  x64, Purchase OK!")
        
    CallSub(Main, "BillingAskProductsAndFillClv")
        
    Return Null
    End Sub

    Sub Service_Start (StartingIntent As Intent)
        
    Service.StopAutomaticForeground 'Starter service can start in the foreground state in some edge cases.
    End Sub

    'Return true to allow the OS default exceptions handler to handle the uncaught exception.
    Sub Application_Error (Error As Exception, StackTrace As StringAs Boolean
        
    Return True
    End Sub

    Sub Service_Destroy

    End Sub

    Prerequisites for use:
    1. Reading and understanding the "GooglePlayBilling - In App Purchases" library
    2. Compile the app and create an entry for the APK in the Playstore (suitable images for "Feature" and "Promo" can be found in the Assets folder for your convenience).
    3. Create a "Closed Track" for the app under "App releases" and set up at least one tester.
    4. Make sure that all requirements for publishing are met (note the checkmarks at the edge of the "Store presence" menu. Most fail by ignoring the small warnings.)
    5. Under "Store presence" activate "FREE" and "PUBLISH". Don't worry, because there are only "Closed" releases, it won't appear in the real Playstore.
    6. If a named entry is not visible or disabled, then somewhere in the whole settings a prerequisite is missing.
    7. Create subscriptions in Google's Developer console. Use the names "subsprod1" and "subsprod2" as ProductId. They are used in the source code. Enter a title and a description and then ACTIVATE the product.
    8. In order to simplify the whole payment process there is the possibility to work with "License Testing" credit cards. Here you can test good and bad payment situations. How the setup works can be found here
    Attached screenshots may be helpful for orientation.
     

    Attached Files:

    Erel, angel_, rboeck and 3 others like this.
Loading...
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice