diff --git a/Branch-SDK/build.gradle.kts b/Branch-SDK/build.gradle.kts
index eb8b792e4..ebbce3de5 100644
--- a/Branch-SDK/build.gradle.kts
+++ b/Branch-SDK/build.gradle.kts
@@ -38,10 +38,12 @@ dependencies {
compileOnly("store.galaxy.samsung.installreferrer:samsung_galaxystore_install_referrer:4.0.0")
// Xiaomi install referrer
compileOnly("com.miui.referrer:homereferrer:1.0.0.7")
+ //implementation(project(":BranchGooglePlayBillingV8"))
- // Google Play Billing library
compileOnly("com.android.billingclient:billing:6.0.1")
+ // Google Play Billing library
+
// In app browser experience
compileOnly("androidx.browser:browser:1.8.0")
diff --git a/Branch-SDK/src/androidTest/java/io/branch/referral/BillingGooglePlayTests.kt b/Branch-SDK/src/androidTest/java/io/branch/referral/BillingGooglePlayTests.kt
index 20b0ad9f2..49dc9c89b 100644
--- a/Branch-SDK/src/androidTest/java/io/branch/referral/BillingGooglePlayTests.kt
+++ b/Branch-SDK/src/androidTest/java/io/branch/referral/BillingGooglePlayTests.kt
@@ -2,6 +2,7 @@ package io.branch.referral
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.billingclient.api.Purchase
+import com.example.branchgoogleplaybillingv8.BillingV8
import io.branch.referral.util.CurrencyType
import org.junit.Assert
@@ -25,7 +26,7 @@ class BillingGooglePlayTests : BranchTest() {
"XDFlSNC9Gqs+PPmO3xOFdLMaQ4FbsBEpTxBuOd+6adEEcz5Uovlgep+F5Xbr08+x/xzCEyNzybDYDcNg/PTzwfoK6Aeq44mocW4CPA1w/r1rdmgtwBD8nAdWIr3BbwXmcl6LYEGA6dL0N+/3zzjNzK/VWdqXazSdRyXxtlHnx8wsBFdPCBs1e9LtEwUcganA6ot0ttO2ySCKYNne2pEm2ScU+uuWZqZJ00VM7KH9pT+SKOOlSs6rRuFEvbGsoPUdybZQ0WoiXg6JD2hz9/35mQJF4Lkjh2kVgTh5MV4sCNnbMuUmhX/d09+pK2Fw6xiUng3FClOetFV9MaTtsmbz/g=="
val mockPurchase = Purchase(purchaseJsonString, purchaseSignature)
- BillingGooglePlay.getInstance().createAndLogEventForPurchase(testContext, mockPurchase, listOf(), CurrencyType.USD, 99.99, "IAP")
+ BillingV8.getInstance().createAndLogEventForPurchase(testContext, mockPurchase, listOf(), CurrencyType.USD, 99.99, "IAP")
val queue = ServerRequestQueue.getInstance(testContext)
val eventRequest = queue.peekAt(0)
diff --git a/Branch-SDK/src/main/java/io/branch/interfaces/GooglePlayBillingWrapper.kt b/Branch-SDK/src/main/java/io/branch/interfaces/GooglePlayBillingWrapper.kt
new file mode 100644
index 000000000..40d500a2d
--- /dev/null
+++ b/Branch-SDK/src/main/java/io/branch/interfaces/GooglePlayBillingWrapper.kt
@@ -0,0 +1,9 @@
+package io.branch.interfaces
+
+import android.content.Context
+
+interface GooglePlayBillingWrapper {
+ fun connect()
+ fun logEventWithPurchase(context: Context, purchase: Any)
+}
+
diff --git a/Branch-SDK/src/main/java/io/branch/referral/Branch.java b/Branch-SDK/src/main/java/io/branch/referral/Branch.java
index f04d64fe9..247ccf81a 100644
--- a/Branch-SDK/src/main/java/io/branch/referral/Branch.java
+++ b/Branch-SDK/src/main/java/io/branch/referral/Branch.java
@@ -6,7 +6,6 @@
import static io.branch.referral.Defines.Jsonkey.EXTERNAL_BROWSER;
import static io.branch.referral.Defines.Jsonkey.IN_APP_WEBVIEW;
import static io.branch.referral.PrefHelper.isValidBranchKey;
-import static io.branch.referral.util.DependencyUtilsKt.billingGooglePlayClass;
import static io.branch.referral.util.DependencyUtilsKt.classExists;
import android.app.Activity;
@@ -50,6 +49,7 @@
import java.util.concurrent.TimeoutException;
import io.branch.indexing.BranchUniversalObject;
+import io.branch.interfaces.GooglePlayBillingWrapper;
import io.branch.interfaces.IBranchLoggingCallbacks;
import io.branch.referral.Defines.PreinstallKey;
import io.branch.referral.ServerRequestGetLATD.BranchLastAttributedTouchDataListener;
@@ -2652,16 +2652,25 @@ public static void notifyNativeToInit(){
}
}
+ private GooglePlayBillingWrapper billingHandler = null;
public void logEventWithPurchase(@NonNull Context context, @NonNull Purchase purchase) {
- if (classExists(billingGooglePlayClass)) {
- BillingGooglePlay.Companion.getInstance().startBillingClient(succeeded -> {
- if (succeeded) {
- BillingGooglePlay.Companion.getInstance().logEventWithPurchase(context, purchase);
- } else {
- BranchLogger.e("Cannot log IAP event. Billing client setup failed"); }
- return null;
- });
- }
+ // New Code Begins
+ billingHandler = GooglePlayBillingManager.INSTANCE.getBillingImplementation();
+
+ if (billingHandler != null) {
+ billingHandler.connect();
+ }
+ // New Code Ends
+
+// if (classExists(billingGooglePlayClass)) {
+// BillingV6.Companion.getInstance().startBillingClient(succeeded -> {
+// if (succeeded) {
+// BillingV6.Companion.getInstance().logEventWithPurchase(context, purchase);
+// } else {
+// BranchLogger.e("Cannot log IAP event. Billing client setup failed"); }
+// return null;
+// });
+// }
}
/**
diff --git a/Branch-SDK/src/main/java/io/branch/referral/BillingGooglePlay.kt b/Branch-SDK/src/main/java/io/branch/referral/GooglePlayBillingLibraryV6.kt
similarity index 91%
rename from Branch-SDK/src/main/java/io/branch/referral/BillingGooglePlay.kt
rename to Branch-SDK/src/main/java/io/branch/referral/GooglePlayBillingLibraryV6.kt
index c068a8e46..4a253f67b 100644
--- a/Branch-SDK/src/main/java/io/branch/referral/BillingGooglePlay.kt
+++ b/Branch-SDK/src/main/java/io/branch/referral/GooglePlayBillingLibraryV6.kt
@@ -1,23 +1,33 @@
package io.branch.referral
import android.content.Context
-import com.android.billingclient.api.*
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.Purchase
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryProductDetailsParams
import io.branch.indexing.BranchUniversalObject
-import io.branch.referral.util.*
+import io.branch.referral.util.BRANCH_STANDARD_EVENT
+import io.branch.referral.util.BranchContentSchema
+import io.branch.referral.util.BranchEvent
+import io.branch.referral.util.ContentMetadata
+import io.branch.referral.util.CurrencyType
import java.math.BigDecimal
-class BillingGooglePlay private constructor() {
+class GooglePlayBillingLibraryV6 private constructor() {
lateinit var billingClient: BillingClient
companion object {
@Volatile
- private lateinit var instance: BillingGooglePlay
+ private lateinit var instance: GooglePlayBillingLibraryV6
- fun getInstance(): BillingGooglePlay {
+ fun getInstance(): GooglePlayBillingLibraryV6 {
synchronized(this) {
if (!::instance.isInitialized) {
- instance = BillingGooglePlay()
+ instance = GooglePlayBillingLibraryV6()
instance.billingClient =
BillingClient.newBuilder(Branch.getInstance().applicationContext)
diff --git a/Branch-SDK/src/main/java/io/branch/referral/GooglePlayBillingManager.kt b/Branch-SDK/src/main/java/io/branch/referral/GooglePlayBillingManager.kt
new file mode 100644
index 000000000..58fef0186
--- /dev/null
+++ b/Branch-SDK/src/main/java/io/branch/referral/GooglePlayBillingManager.kt
@@ -0,0 +1,24 @@
+package io.branch.referral
+
+import io.branch.interfaces.GooglePlayBillingWrapper
+
+object GooglePlayBillingManager {
+ fun getBillingImplementation(): GooglePlayBillingWrapper? {
+ // Try to load V8 first
+ try {
+ val clazz = Class.forName("com.branch.billing.v8.BillingV8Implementation")
+ return clazz.getConstructor().newInstance() as GooglePlayBillingWrapper
+ } catch (e: ClassNotFoundException) {
+ // V8 not found, try V6
+ }
+
+ try {
+ val clazz = Class.forName("com.branch.billing.v6.BillingV6Implementation")
+ return clazz.getConstructor().newInstance() as GooglePlayBillingWrapper
+ } catch (e: ClassNotFoundException) {
+ // Neither version is linked in the user's app
+ BranchLogger.e("No Billing Library dependency found!")
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/.gitignore b/BranchGooglePlayBillingV8/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/BranchGooglePlayBillingV8/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/build.gradle.kts b/BranchGooglePlayBillingV8/build.gradle.kts
new file mode 100644
index 000000000..711006ccf
--- /dev/null
+++ b/BranchGooglePlayBillingV8/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.example.branchgoogleplaybillingv8"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.17.0")
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
+ implementation("androidx.appcompat:appcompat:1.7.1")
+ implementation("com.google.android.material:material:1.13.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ // Google Play Billing library
+ compileOnly("com.android.billingclient:billing:8.0.0")
+
+ // Branch SDK Implementations
+ implementation(project(":Branch-SDK"))
+}
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/consumer-rules.pro b/BranchGooglePlayBillingV8/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/BranchGooglePlayBillingV8/proguard-rules.pro b/BranchGooglePlayBillingV8/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/BranchGooglePlayBillingV8/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/src/androidTest/java/com/example/branchgoogleplaybillingv8/ExampleInstrumentedTest.kt b/BranchGooglePlayBillingV8/src/androidTest/java/com/example/branchgoogleplaybillingv8/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..387da5259
--- /dev/null
+++ b/BranchGooglePlayBillingV8/src/androidTest/java/com/example/branchgoogleplaybillingv8/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.branchgoogleplaybillingv8
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.branchgoogleplaybillingv8.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/src/main/AndroidManifest.xml b/BranchGooglePlayBillingV8/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..a5918e68a
--- /dev/null
+++ b/BranchGooglePlayBillingV8/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/src/main/java/com/example/branchgoogleplaybillingv8/BillingV8.kt b/BranchGooglePlayBillingV8/src/main/java/com/example/branchgoogleplaybillingv8/BillingV8.kt
new file mode 100644
index 000000000..b8322dc98
--- /dev/null
+++ b/BranchGooglePlayBillingV8/src/main/java/com/example/branchgoogleplaybillingv8/BillingV8.kt
@@ -0,0 +1,276 @@
+package com.example.branchgoogleplaybillingv8
+
+import android.content.Context
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.Purchase
+import com.android.billingclient.api.QueryProductDetailsParams
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.PendingPurchasesParams
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryProductDetailsResult
+import io.branch.indexing.BranchUniversalObject
+import io.branch.referral.Branch
+import io.branch.referral.BranchLogger
+import io.branch.referral.util.BRANCH_STANDARD_EVENT
+import io.branch.referral.util.BranchContentSchema
+import io.branch.referral.util.BranchEvent
+import io.branch.referral.util.ContentMetadata
+import io.branch.referral.util.CurrencyType
+import java.math.BigDecimal
+
+class BillingV8 private constructor() {
+
+ lateinit var billingClient: BillingClient
+
+ companion object {
+ @Volatile
+ private lateinit var instance: BillingV8
+
+ fun getInstance(): BillingV8 {
+ synchronized(this) {
+ if (!::instance.isInitialized) {
+ instance = BillingV8()
+
+ instance.billingClient =
+ BillingClient.newBuilder(Branch.getInstance().applicationContext)
+ .setListener(instance.purchasesUpdatedListener)
+ .enablePendingPurchases(
+ PendingPurchasesParams.newBuilder()
+ .enableOneTimeProducts()
+ .build()
+ )
+ .build()
+ }
+ return instance
+ }
+ }
+ }
+
+ fun startBillingClient(callback: (Boolean) -> Unit) {
+ if (billingClient.isReady) {
+ BranchLogger.v("Billing Client has already been started..")
+ callback(true)
+ } else {
+ billingClient.startConnection(object : BillingClientStateListener {
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+ BranchLogger.v("Billing Client setup finished.")
+ callback(true)
+ } else {
+ val errorMessage =
+ "Billing Client setup failed with error: ${billingResult.debugMessage}"
+ BranchLogger.e(errorMessage)
+ callback(false)
+ }
+ }
+
+ override fun onBillingServiceDisconnected() {
+ BranchLogger.w("Billing Client disconnected")
+ callback(false)
+ }
+ })
+ }
+ }
+
+ private val purchasesUpdatedListener = PurchasesUpdatedListener { _, _ -> }
+
+ /**
+ * Logs a Branch Commerce Event based on an in-app purchase
+ *
+ * @param context Current context
+ * @param purchase Respective purchase
+ */
+ fun logEventWithPurchase(context: Context, purchase: Purchase) {
+ val productIds = purchase.products
+ val productList: MutableList = ArrayList()
+ val subsList: MutableList = ArrayList()
+
+ for (productId: String? in productIds) {
+ val inAppProduct = QueryProductDetailsParams.Product.newBuilder()
+ .setProductId(productId!!)
+ .setProductType(BillingClient.ProductType.INAPP)
+ .build()
+ productList.add(inAppProduct)
+
+ val subsProduct = QueryProductDetailsParams.Product.newBuilder()
+ .setProductId(productId)
+ .setProductType(BillingClient.ProductType.SUBS)
+ .build()
+ subsList.add(subsProduct)
+ }
+
+ val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
+ .setProductList(productList)
+ .build()
+
+ val querySubsProductDetailsParams = QueryProductDetailsParams.newBuilder()
+ .setProductList(subsList)
+ .build()
+
+ billingClient.queryProductDetailsAsync(
+ querySubsProductDetailsParams
+ ) { billingResult: BillingResult, subsProductDetailsList: QueryProductDetailsResult ->
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+ val contentItemBUOs: MutableList =
+ ArrayList()
+ var currency: CurrencyType = CurrencyType.USD
+ var revenue = 0.00
+
+ for (product: ProductDetails? in subsProductDetailsList.productDetailsList) {
+ val buo: BranchUniversalObject = createBUOWithSubsProductDetails(product)
+ contentItemBUOs.add(buo)
+
+ revenue += buo.contentMetadata.price
+ currency = buo.contentMetadata.currencyType
+ }
+
+ if (contentItemBUOs.isNotEmpty()) {
+ createAndLogEventForPurchase(
+ context,
+ purchase,
+ contentItemBUOs,
+ currency,
+ revenue,
+ BillingClient.ProductType.SUBS
+ )
+ }
+ }
+ else {
+ BranchLogger.e("Failed to query subscriptions. Error: " + billingResult.debugMessage)
+ }
+ }
+
+ billingClient.queryProductDetailsAsync(
+ queryProductDetailsParams
+ ) { billingResult: BillingResult, productDetailsList: QueryProductDetailsResult ->
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+
+ val contentItemBUOs: MutableList =
+ ArrayList()
+ var currency: CurrencyType = CurrencyType.USD
+ var revenue = 0.00
+ val quantity: Int = purchase.quantity
+
+ for (product: ProductDetails? in productDetailsList.productDetailsList) {
+ val buo: BranchUniversalObject =
+ createBUOWithInAppProductDetails(product, quantity)
+ contentItemBUOs.add(buo)
+
+ revenue += (BigDecimal(buo.contentMetadata.price.toString()) * BigDecimal(
+ quantity.toString()
+ )).toDouble()
+ currency = buo.contentMetadata.currencyType
+ }
+
+ if (contentItemBUOs.isNotEmpty()) {
+ createAndLogEventForPurchase(
+ context,
+ purchase,
+ contentItemBUOs,
+ currency,
+ revenue,
+ BillingClient.ProductType.INAPP
+ )
+ }
+ }
+ else {
+ BranchLogger.e("Failed to query subscriptions. Error: " + billingResult.debugMessage)
+ }
+ }
+ }
+
+ private fun createBUOWithSubsProductDetails(product: ProductDetails?): BranchUniversalObject {
+ if (product != null) {
+
+ val pricingPhaseList =
+ product.subscriptionOfferDetails?.get(0)?.pricingPhases?.pricingPhaseList?.get(0)
+
+ val currency = pricingPhaseList?.let {
+ CurrencyType.valueOf(
+ it.priceCurrencyCode
+ )
+ }
+
+ val price = pricingPhaseList?.priceAmountMicros?.div(1000000.0)
+
+ val buo = BranchUniversalObject()
+ .setCanonicalIdentifier(product.productId)
+ .setTitle(product.title)
+
+ val contentMetadata = ContentMetadata()
+ .addCustomMetadata("product_type", product.productType)
+ .setProductName(product.name)
+ .setQuantity(1.0)
+ .setContentSchema(BranchContentSchema.COMMERCE_PRODUCT)
+
+ if (price != null && currency != null) {
+ contentMetadata.setPrice(price, currency);
+ }
+
+ buo.contentMetadata = contentMetadata;
+
+ return buo;
+ } else {
+ return BranchUniversalObject()
+ }
+ }
+
+ private fun createBUOWithInAppProductDetails(
+ product: ProductDetails?,
+ quantity: Int
+ ): BranchUniversalObject {
+ if (product != null) {
+
+ val currency = product.oneTimePurchaseOfferDetails?.priceCurrencyCode?.let {
+ CurrencyType.valueOf(it)
+ }
+ val price = product.oneTimePurchaseOfferDetails?.priceAmountMicros?.div(1000000.0)
+
+ val buo = BranchUniversalObject()
+ .setCanonicalIdentifier(product.productId)
+ .setTitle(product.title)
+
+ val contentMetadata = ContentMetadata()
+ .addCustomMetadata("product_type", product.productType)
+ .setProductName(product.name)
+ .setQuantity(quantity.toDouble())
+ .setContentSchema(BranchContentSchema.COMMERCE_PRODUCT)
+
+ if (price != null && currency != null) {
+ contentMetadata.setPrice(price, currency);
+ }
+
+ buo.contentMetadata = contentMetadata;
+
+ return buo;
+ } else {
+ return BranchUniversalObject()
+ }
+ }
+
+ fun createAndLogEventForPurchase(
+ context: Context,
+ purchase: Purchase,
+ contentItems: List,
+ currency: CurrencyType,
+ revenue: Double,
+ productType: String
+ ) {
+ BranchEvent(BRANCH_STANDARD_EVENT.PURCHASE)
+ .setCurrency(currency)
+ .setDescription(purchase.orderId)
+ .setCustomerEventAlias(productType)
+ .setRevenue(revenue)
+ .addCustomDataProperty("package_name", purchase.packageName)
+ .addCustomDataProperty("order_id", purchase.orderId)
+ .addCustomDataProperty("logged_from_IAP", "true")
+ .addCustomDataProperty("is_auto_renewing", purchase.isAutoRenewing.toString())
+ .addCustomDataProperty("purchase_token", purchase.purchaseToken)
+ .addContentItems(contentItems)
+ .logEvent(context)
+
+ BranchLogger.i("Successfully logged in-app purchase as Branch Event")
+ }
+}
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/src/main/java/com/example/branchgoogleplaybillingv8/BillingV8Implementation.kt b/BranchGooglePlayBillingV8/src/main/java/com/example/branchgoogleplaybillingv8/BillingV8Implementation.kt
new file mode 100644
index 000000000..3d073e001
--- /dev/null
+++ b/BranchGooglePlayBillingV8/src/main/java/com/example/branchgoogleplaybillingv8/BillingV8Implementation.kt
@@ -0,0 +1,268 @@
+package com.example.branchgoogleplaybillingv8
+
+import android.content.Context
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.PendingPurchasesParams
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.Purchase
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryProductDetailsParams
+import com.android.billingclient.api.QueryProductDetailsResult
+import io.branch.indexing.BranchUniversalObject
+import io.branch.interfaces.GooglePlayBillingWrapper
+import io.branch.referral.Branch
+import io.branch.referral.BranchLogger
+import io.branch.referral.util.BRANCH_STANDARD_EVENT
+import io.branch.referral.util.BranchContentSchema
+import io.branch.referral.util.BranchEvent
+import io.branch.referral.util.ContentMetadata
+import io.branch.referral.util.CurrencyType
+import java.math.BigDecimal
+
+class BillingV8Implementation : GooglePlayBillingWrapper {
+
+ lateinit var billingClient: BillingClient
+
+ override fun connect() {
+ if (!::billingClient.isInitialized) {
+ billingClient = BillingClient.newBuilder(Branch.getInstance().applicationContext)
+ .setListener(purchasesUpdatedListener)
+ .enablePendingPurchases(
+ PendingPurchasesParams.newBuilder()
+ .enableOneTimeProducts()
+ .build()
+ )
+ .build()
+ }
+ if (billingClient.isReady) {
+ BranchLogger.v("Billing Client already ready.")
+ } else {
+ billingClient.startConnection(object : BillingClientStateListener {
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+ BranchLogger.v("Billing Client setup finished.")
+ } else {
+ val errorMessage =
+ "Billing Client setup failed with error: ${billingResult.debugMessage}"
+ BranchLogger.e(errorMessage)
+ }
+ }
+
+ override fun onBillingServiceDisconnected() {
+ BranchLogger.w("Billing Client disconnected")
+ }
+ })
+ }
+ }
+
+ override fun logEventWithPurchase(context: Context, purchase: Any) {
+ if (purchase is Purchase) {
+ handlePurchaseLogic(context, purchase)
+ } else {
+ BranchLogger.e("BillingV8 Object passed is not valid Purchase object.")
+ }
+ }
+
+ private val purchasesUpdatedListener = PurchasesUpdatedListener { _, _ -> }
+
+ /**
+ * Logs a Branch Commerce Event based on an in-app purchase
+ *
+ * @param context Current context
+ * @param purchase Respective purchase
+ */
+
+ private fun handlePurchaseLogic(context: Context, purchase: Purchase) {
+ val productIds = purchase.products
+ val productList: MutableList = ArrayList()
+ val subsList: MutableList = ArrayList()
+
+ for (productId: String? in productIds) {
+ val inAppProduct = QueryProductDetailsParams.Product.newBuilder()
+ .setProductId(productId!!)
+ .setProductType(BillingClient.ProductType.INAPP)
+ .build()
+ productList.add(inAppProduct)
+
+ val subsProduct = QueryProductDetailsParams.Product.newBuilder()
+ .setProductId(productId)
+ .setProductType(BillingClient.ProductType.SUBS)
+ .build()
+ subsList.add(subsProduct)
+ }
+
+ val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
+ .setProductList(productList)
+ .build()
+
+ val querySubsProductDetailsParams = QueryProductDetailsParams.newBuilder()
+ .setProductList(subsList)
+ .build()
+
+ billingClient.queryProductDetailsAsync(
+ querySubsProductDetailsParams
+ ) { billingResult: BillingResult, subsProductDetailsList: QueryProductDetailsResult ->
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+ val contentItemBUOs: MutableList =
+ ArrayList()
+ var currency: CurrencyType = CurrencyType.USD
+ var revenue = 0.00
+
+ for (product: ProductDetails? in subsProductDetailsList.productDetailsList) {
+ val buo: BranchUniversalObject = createBUOWithSubsProductDetails(product)
+ contentItemBUOs.add(buo)
+
+ revenue += buo.contentMetadata.price
+ currency = buo.contentMetadata.currencyType
+ }
+
+ if (contentItemBUOs.isNotEmpty()) {
+ createAndLogEventForPurchase(
+ context,
+ purchase,
+ contentItemBUOs,
+ currency,
+ revenue,
+ BillingClient.ProductType.SUBS
+ )
+ }
+ }
+ else {
+ BranchLogger.e("Failed to query subscriptions. Error: " + billingResult.debugMessage)
+ }
+ }
+
+ billingClient.queryProductDetailsAsync(
+ queryProductDetailsParams
+ ) { billingResult: BillingResult, productDetailsList: QueryProductDetailsResult ->
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+
+ val contentItemBUOs: MutableList =
+ ArrayList()
+ var currency: CurrencyType = CurrencyType.USD
+ var revenue = 0.00
+ val quantity: Int = purchase.quantity
+
+ for (product: ProductDetails? in productDetailsList.productDetailsList) {
+ val buo: BranchUniversalObject =
+ createBUOWithInAppProductDetails(product, quantity)
+ contentItemBUOs.add(buo)
+
+ revenue += (BigDecimal(buo.contentMetadata.price.toString()) * BigDecimal(
+ quantity.toString()
+ )).toDouble()
+ currency = buo.contentMetadata.currencyType
+ }
+
+ if (contentItemBUOs.isNotEmpty()) {
+ createAndLogEventForPurchase(
+ context,
+ purchase,
+ contentItemBUOs,
+ currency,
+ revenue,
+ BillingClient.ProductType.INAPP
+ )
+ }
+ }
+ else {
+ BranchLogger.e("Failed to query subscriptions. Error: " + billingResult.debugMessage)
+ }
+ }
+ }
+
+ private fun createBUOWithSubsProductDetails(product: ProductDetails?): BranchUniversalObject {
+ if (product != null) {
+
+ val pricingPhaseList =
+ product.subscriptionOfferDetails?.get(0)?.pricingPhases?.pricingPhaseList?.get(0)
+
+ val currency = pricingPhaseList?.let {
+ CurrencyType.valueOf(
+ it.priceCurrencyCode
+ )
+ }
+
+ val price = pricingPhaseList?.priceAmountMicros?.div(1000000.0)
+
+ val buo = BranchUniversalObject()
+ .setCanonicalIdentifier(product.productId)
+ .setTitle(product.title)
+
+ val contentMetadata = ContentMetadata()
+ .addCustomMetadata("product_type", product.productType)
+ .setProductName(product.name)
+ .setQuantity(1.0)
+ .setContentSchema(BranchContentSchema.COMMERCE_PRODUCT)
+
+ if (price != null && currency != null) {
+ contentMetadata.setPrice(price, currency);
+ }
+
+ buo.contentMetadata = contentMetadata;
+
+ return buo;
+ } else {
+ return BranchUniversalObject()
+ }
+ }
+
+ private fun createBUOWithInAppProductDetails(
+ product: ProductDetails?,
+ quantity: Int
+ ): BranchUniversalObject {
+ if (product != null) {
+
+ val currency = product.oneTimePurchaseOfferDetails?.priceCurrencyCode?.let {
+ CurrencyType.valueOf(it)
+ }
+ val price = product.oneTimePurchaseOfferDetails?.priceAmountMicros?.div(1000000.0)
+
+ val buo = BranchUniversalObject()
+ .setCanonicalIdentifier(product.productId)
+ .setTitle(product.title)
+
+ val contentMetadata = ContentMetadata()
+ .addCustomMetadata("product_type", product.productType)
+ .setProductName(product.name)
+ .setQuantity(quantity.toDouble())
+ .setContentSchema(BranchContentSchema.COMMERCE_PRODUCT)
+
+ if (price != null && currency != null) {
+ contentMetadata.setPrice(price, currency);
+ }
+
+ buo.contentMetadata = contentMetadata;
+
+ return buo;
+ } else {
+ return BranchUniversalObject()
+ }
+ }
+
+ fun createAndLogEventForPurchase(
+ context: Context,
+ purchase: Purchase,
+ contentItems: List,
+ currency: CurrencyType,
+ revenue: Double,
+ productType: String
+ ) {
+ BranchEvent(BRANCH_STANDARD_EVENT.PURCHASE)
+ .setCurrency(currency)
+ .setDescription(purchase.orderId)
+ .setCustomerEventAlias(productType)
+ .setRevenue(revenue)
+ .addCustomDataProperty("package_name", purchase.packageName)
+ .addCustomDataProperty("order_id", purchase.orderId)
+ .addCustomDataProperty("logged_from_IAP", "true")
+ .addCustomDataProperty("is_auto_renewing", purchase.isAutoRenewing.toString())
+ .addCustomDataProperty("purchase_token", purchase.purchaseToken)
+ .addContentItems(contentItems)
+ .logEvent(context)
+
+ BranchLogger.i("Successfully logged in-app purchase as Branch Event")
+ }
+}
\ No newline at end of file
diff --git a/BranchGooglePlayBillingV8/src/test/java/com/example/branchgoogleplaybillingv8/ExampleUnitTest.kt b/BranchGooglePlayBillingV8/src/test/java/com/example/branchgoogleplaybillingv8/ExampleUnitTest.kt
new file mode 100644
index 000000000..bcabc5da1
--- /dev/null
+++ b/BranchGooglePlayBillingV8/src/test/java/com/example/branchgoogleplaybillingv8/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.branchgoogleplaybillingv8
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 91c7cfd6c..521c6f5a6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -9,3 +9,4 @@ pluginManagement {
mavenCentral()
}
}
+include(":BranchGooglePlayBillingV8")