Android Tutorial Perform GooglePlayBilling Subscription Up/Downgrade


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.


' ~~~~~~~~~~~~~~~~~~~~
' ~~~~~~~~~~~~~~~~~~~~
'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:
<uses-sdk android:minSdkVersion="15" android:targetSdkVersion="28"/>
<supports-screens android:largeScreens="true"
SetApplicationAttribute(android:icon, "@drawable/icon")
SetApplicationAttribute(android:label, "$LABEL$")
CreateResourceFromFile(Macro, Themes.DarkTheme)
'End of default text.
CreateResourceFromFile(Macro, GooglePlayBilling.GooglePlayBilling)

' ~~~~~~~~~~~~~~~~~~~~
' ~~~~~~~~~~~~~~~~~~~~
#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 -->

' Google's howto -->

' User-test a Google Play Billing app -->

' Google Test subscriptions-specific features -->
#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.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
    Dim BillingSubProduct As BillingSubProduct
    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.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.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
    BillingActiveCostLevel = 0
    Dim sb As StringBuilder
    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, ")
            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("#-  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
                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
            End If
            LabelAskProducts.Text = sb.ToString
        End If
        LabelAskProducts.Text = "Not connected"
        Log("#-  x139, not connected")
    End If
    Return Null
End Sub
private Sub FillClv_Products
    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
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.SetLayout(0, 0, BillingProductsClv.AsView.Width, PanelHeight)
    clvitemBackPanel.SetLayoutAnimated(0, 8dip, 0, BillingProductsClv.AsView.Width -16dip, PanelHeight -8dip)
    clvitemLabel1.SetLayoutAnimated(0, 0, 0, BillingProductsClv.AsView.Width -16dip, 40dip)
    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"
        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"
                clvitemButton1.Text = "DOWNgrade"
            End If
            clvitemButton1.Text = "BUY"
        End If
    End If
    If bsp.ProductID = "free" Then
    End If
    Return p
End Sub

Sub clvitemButton1_Click As ResumableSub
    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", "", "", Null, True )
    Wait For (sf) Msgbox_Result (intIlResult As Int)

        StartActivity (Starter.pi.OpenBrowser ("") )
    else If bspSelectedByUser.ProductID = BillingActiveProductId Then
        StartActivity (Starter.pi.OpenBrowser ("") )
        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)
                '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
                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 String) As BillingResult
    ' Google -->
    ' Erel -->
    '   should be called from the activity
    Dim jo As JavaObject = Starter.billing
    Dim BillingClient As JavaObject = jo.GetField("client")
    Dim context As JavaObject
    Dim BillingFlowParams As JavaObject
    BillingFlowParams = BillingFlowParams.InitializeStatic("") _
               .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

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

' ~~~~~~~~~~~~~~~~~~~~
' ~~~~~~~~~~~~~~~~~~~~
#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(3) As String
    Public BillingActiveSubsProductTitle As String = ""
    Public pi As PhoneIntents
End Sub

Sub Service_Create
    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
                Log("#-    x43, Unexpected product...")
            End If
    End If
End Sub

Private Sub HandleSubscriptionPurchase (p As Purchase) As ResumableSub
    Log("#-Sub starter.Sub HandleSubscriptionPurchase, p.sku=" & p.Sku)
    If p.PurchaseState <> p.STATE_PURCHASED Then Return Null
    ' -->
    '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 String) As 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.