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")