From 7273dad6cd30845a5fcb782659929acb3a1b0266 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Sat, 20 Feb 2021 09:55:28 +0300 Subject: [PATCH 01/11] APH-400 - isDebuggable () function was introduced for proper sandbox detection process --- sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 6ebd5b70..66ad3d13 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -2,6 +2,7 @@ package com.apphud.sdk import android.app.Activity import android.content.Context +import android.content.pm.ApplicationInfo import android.os.AsyncTask import android.os.Build import android.os.Handler @@ -372,7 +373,7 @@ internal object ApphudInternal { user_id = userId, device_id = deviceId, time_zone = TimeZone.getDefault().id, - is_sandbox = BuildConfig.DEBUG + is_sandbox = isDebuggable(context) ) internal fun getSkuDetailsList(): MutableList? { @@ -382,4 +383,8 @@ internal object ApphudInternal { internal fun getSkuDetailsByProductId(productIdentifier: String): SkuDetails? { return getSkuDetailsList()?.let { skuList -> skuList.firstOrNull { it.sku == productIdentifier } } } + + private fun isDebuggable(ctx: Context): Boolean { + return 0 != ctx.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE + } } \ No newline at end of file From e8c8d4fd7719cc14d08b9bcadda33bae7fe477da Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Mon, 22 Feb 2021 17:11:41 +0300 Subject: [PATCH 02/11] APH-424 - New field 'is_new' was added to 'RegistrationBody' --- sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt | 7 +++++-- sdk/src/main/java/com/apphud/sdk/body/RegistrationBody.kt | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 66ad3d13..74c47c06 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -54,6 +54,8 @@ internal object ApphudInternal { internal var userId: UserId? = null private lateinit var deviceId: DeviceId + private var is_new = true + internal lateinit var apiKey: ApiKey internal lateinit var context: Context @@ -318,7 +320,7 @@ internal object ApphudInternal { private fun updateDevice(id: DeviceId?): DeviceId { val deviceId = when (id) { - null -> storage.deviceId ?: generatedUUID + null -> storage.deviceId?.let { is_new = false; it } ?: generatedUUID else -> id } storage.deviceId = deviceId @@ -373,7 +375,8 @@ internal object ApphudInternal { user_id = userId, device_id = deviceId, time_zone = TimeZone.getDefault().id, - is_sandbox = isDebuggable(context) + is_sandbox = isDebuggable(context), + is_new = this.is_new ) internal fun getSkuDetailsList(): MutableList? { diff --git a/sdk/src/main/java/com/apphud/sdk/body/RegistrationBody.kt b/sdk/src/main/java/com/apphud/sdk/body/RegistrationBody.kt index a742b336..bdaa1962 100644 --- a/sdk/src/main/java/com/apphud/sdk/body/RegistrationBody.kt +++ b/sdk/src/main/java/com/apphud/sdk/body/RegistrationBody.kt @@ -14,5 +14,6 @@ data class RegistrationBody( val user_id: String?, val device_id: String, val time_zone: String, - val is_sandbox: Boolean + val is_sandbox: Boolean, + val is_new: Boolean ) \ No newline at end of file From 79bd0d4f24aa0cf9e631ee8018295f678ce7d334 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Mon, 22 Feb 2021 17:37:05 +0300 Subject: [PATCH 03/11] APH-401 - The initial launch process has been redesigned. The list of products is now loaded almost simultaneously with registration. A trigger has also been added to prevent multiple accidental initializations on startup --- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 27 ++----- .../java/com/apphud/sdk/ApphudInternal.kt | 81 +++++++++++++------ .../java/com/apphud/sdk/ApphudListener.kt | 6 +- .../main/java/com/apphud/sdk/ApphudUtils.kt | 2 +- .../com/apphud/sdk/client/ApphudClient.kt | 6 +- 5 files changed, 71 insertions(+), 51 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 3609c421..2ad38e1f 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -1,10 +1,7 @@ package com.apphud.sdk import android.app.Activity -import android.app.ActivityManager import android.content.Context -import android.os.Process -import android.util.Log import com.android.billingclient.api.Purchase import com.android.billingclient.api.SkuDetails import com.apphud.sdk.domain.ApphudNonRenewingPurchase @@ -32,7 +29,7 @@ object Apphud { fun start(context: Context, apiKey: ApiKey, userId: UserId? = null) = start(context, apiKey, userId, null) - private fun isMainProcess(context: Context): Boolean = +/* private fun isMainProcess(context: Context): Boolean = context.packageName == getProcessName(context) private fun getProcessName(context: Context): String? { @@ -47,7 +44,7 @@ object Apphud { } null } - } + }*/ /** * Initializes Apphud SDK. You should call it during app launch. @@ -57,21 +54,11 @@ object Apphud { * @parameter deviceID: Optional. You can provide your own unique device identifier. If null passed then UUID will be generated instead. */ @kotlin.jvm.JvmStatic - fun start( - context: Context, - apiKey: ApiKey, - userId: UserId? = null, - deviceId: DeviceId? = null - ) { - if (isMainProcess(context)) { - ApphudInternal.apiKey = apiKey - ApphudInternal.context = context - ApphudInternal.loadAdsId() - ApphudInternal.registration(userId, deviceId) - Log.d("APP_HUD", "start SDK") - } else { - Log.d("APP_HUD", "will not start - only main process is supported by SDK") - } + fun start(context: Context, apiKey: ApiKey, userId: UserId? = null, deviceId: DeviceId? = null) + { + ApphudInternal.apiKey = apiKey + ApphudInternal.context = context + ApphudInternal.initialize(userId, deviceId) } /** diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 74c47c06..40877a7d 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -47,10 +47,14 @@ internal object ApphudInternal { field = value if (storage.advertisingId != value) { storage.advertisingId = value - updateRegistration() + ApphudLog.log("advertisingId = $advertisingId is fetched and saved") } + ApphudLog.log("advertisingId: continue registration") + updateRegistration() } + private var allowIdentifyUser = true + internal var userId: UserId? = null private lateinit var deviceId: DeviceId @@ -65,7 +69,7 @@ internal object ApphudInternal { private val skuDetails = mutableListOf() - internal fun loadAdsId() { + private fun loadAdsId() { if (ApphudUtils.adTracking) { AdvertisingTask().execute() } @@ -79,41 +83,69 @@ internal object ApphudInternal { } internal fun updateUserId(userId: UserId) { + ApphudLog.log("Start updateUserId userId=$userId" ) val id = updateUser(id = userId) this.userId = id val body = mkRegistrationBody(id, deviceId) client.registrationUser(body) { customer -> - handler.post { storage.customer = customer } + handler.post { + storage.customer = customer + ApphudLog.log("End updateUserId customer=${customer.toString()}" ) + } } } - internal fun registration( + internal fun initialize( userId: UserId?, deviceId: DeviceId?, isFetchProducts: Boolean = true - ) { - val id = updateUser(id = userId) - this.userId = id + ){ + if(!allowIdentifyUser){ + ApphudLog.log("=============================================================" + + "\nAbort initializing, because Apphud SDK already initialized." + + "\nYou can only call `Apphud.start()` once per app lifecycle." + + "\nOr if `Apphud.logout()` was called previously." + + "\n=============================================================") + return + } + allowIdentifyUser = false + // try to continue anyway, because maybe already has cached data, try to fetch play market products + fetchProducts() + + ApphudLog.log("Start initialize with userId=$userId, deviceId=$deviceId" ) + this.userId = updateUser(id = userId) this.deviceId = updateDevice(id = deviceId) + ApphudLog.log("Start initialize with saved userId=${this.userId}, saved deviceId=${this.deviceId}") + if (ApphudUtils.adTracking) + loadAdsId() + else + registration(this.userId, this.deviceId) + } + + private fun registration( + userId: UserId?, + deviceId: DeviceId? + ) { + ApphudLog.log("Start registration userId=$userId, deviceId=$deviceId" ) - val body = mkRegistrationBody(id, this.deviceId) + val body = mkRegistrationBody(userId!!, this.deviceId) client.registrationUser(body) { customer -> handler.post { + ApphudLog.log("registration registrationUser customer=${customer.toString()}" ) storage.customer = customer apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) - } - } - if (isFetchProducts) { - // try to continue anyway, because maybe already has cached data, try to fetch products - fetchProducts() - // try to resend purchases, if prev requests was fail - if (storage.isNeedSync) { - syncPurchases() + // try to resend purchases, if prev requests was fail + if (storage.isNeedSync) { + ApphudLog.log("registration syncPurchases" ) + syncPurchases() + } } } + + ApphudLog.log("End registration" ) } internal fun purchase( @@ -173,10 +205,10 @@ internal object ApphudInternal { storage.isNeedSync = true billing.restoreCallback = { records -> - ApphudLog.log("$records") + ApphudLog.log("syncPurchases: $records") when { - prevPurchases.containsAll(records) -> ApphudLog.log("Don't send equal purchases from prev state") + prevPurchases.containsAll(records) -> ApphudLog.log("syncPurchases: Don't send equal purchases from prev state") else -> client.purchased(mkPurchaseBody(records)) { customer -> handler.post { prevPurchases.addAll(records) @@ -184,12 +216,12 @@ internal object ApphudInternal { apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) } - ApphudLog.log("success send history purchases $records") + ApphudLog.log("syncPurchases: success send history purchases $records") } } } billing.historyCallback = { purchases -> - ApphudLog.log("history purchases: $purchases") + ApphudLog.log("syncPurchases: history purchases: $purchases") billing.restore(BillingClient.SkuType.SUBS, purchases) billing.restore(BillingClient.SkuType.INAPP, purchases) } @@ -288,19 +320,19 @@ internal object ApphudInternal { userId = null generatedUUID = UUID.randomUUID().toString() prevPurchases.clear() + skuDetails.clear() } private fun fetchProducts() { billing.skuCallback = { details -> - ApphudLog.log("details: $details") + ApphudLog.log("fetchProducts: details from Google Billing: $details") if (details.isNotEmpty()) { - skuDetails.clear() skuDetails.addAll(details) apphudListener?.apphudFetchSkuDetailsProducts(details) } } client.allProducts { products -> - ApphudLog.log("products: $products") + ApphudLog.log("fetchProducts: products from Apphud server: $products") val ids = products.map { it.productId } billing.details(BillingClient.SkuType.SUBS, ids) billing.details(BillingClient.SkuType.INAPP, ids) @@ -327,8 +359,7 @@ internal object ApphudInternal { return deviceId } - private fun updateRegistration() = - registration(userId, deviceId, isFetchProducts = false) + private fun updateRegistration() = registration(userId, deviceId) private fun mkPurchasesBody(purchases: List) = PurchaseBody( diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudListener.kt b/sdk/src/main/java/com/apphud/sdk/ApphudListener.kt index 4047f65b..c84d7274 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudListener.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudListener.kt @@ -7,8 +7,10 @@ import com.apphud.sdk.domain.ApphudSubscription interface ApphudListener { /** - * Returns array of subscriptions that user ever purchased. Empty array means user never purchased a subscription. If you have just one subscription group in your app, you will always receive just one subscription in an array. - * This method is called when subscription is purchased or updated (for example, status changed from `trial` to `expired` or `isAutorenewEnabled` changed to `false`). + * Returns array of subscriptions that user ever purchased. Empty array means user never purchased a subscription. + * If you have just one subscription group in your app, you will always receive just one subscription in an array. + * This method is called when subscription is purchased or updated + * (for example, status changed from `trial` to `expired` or `isAutorenewEnabled` changed to `false`). * SDK also checks for subscription updates when app becomes active. */ fun apphudSubscriptionsUpdated(subscriptions: List) = Unit diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudUtils.kt b/sdk/src/main/java/com/apphud/sdk/ApphudUtils.kt index 60e4ca29..836c954c 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudUtils.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudUtils.kt @@ -19,6 +19,6 @@ internal object ApphudUtils { } fun disableAdTracking() { - adTracking = true + adTracking = false } } \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt b/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt index 2030de51..8efc2a25 100644 --- a/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt +++ b/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt @@ -15,7 +15,7 @@ import com.apphud.sdk.tasks.* internal class ApphudClient(apiKey: ApiKey, parser: Parser) { //TODO Про эти мапперы класс ApphudClient знать не должен - private val mapper = CustomerMapper(SubscriptionMapper()) + private val customerMapper = CustomerMapper(SubscriptionMapper()) private val productMapper = ProductMapper() private val attributionMapper = AttributionMapper() @@ -28,7 +28,7 @@ internal class ApphudClient(apiKey: ApiKey, parser: Parser) { thread.registration(callable) { response -> when (response.data.results) { null -> ApphudLog.log("Response success but result is null") - else -> callback.invoke(mapper.map(response.data.results)) + else -> callback.invoke(customerMapper.map(response.data.results)) } } } @@ -68,7 +68,7 @@ internal class ApphudClient(apiKey: ApiKey, parser: Parser) { thread.execute(LoopRunnable(callable) { response -> when (response.data.results) { null -> ApphudLog.log("Response success but result is null") - else -> callback.invoke(mapper.map(response.data.results)) + else -> callback.invoke(customerMapper.map(response.data.results)) } }) } From 3ff5d9f754c80e662187cbc338089c00398f8d4d Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Mon, 22 Feb 2021 17:44:27 +0300 Subject: [PATCH 04/11] APH-401 - Small fix for correct logout --- sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 40877a7d..d3671812 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -321,6 +321,7 @@ internal object ApphudInternal { generatedUUID = UUID.randomUUID().toString() prevPurchases.clear() skuDetails.clear() + allowIdentifyUser = true } private fun fetchProducts() { From 036c191f7e5268e800b026c4a6c0d76df63bd94b Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Mon, 22 Feb 2021 20:39:59 +0300 Subject: [PATCH 05/11] APH-401 - Experimental fixes to improve SkuDetailsWrapper async --- .../apphud/sdk/internal/BaseAsyncWrapper.kt | 9 +++ .../apphud/sdk/internal/SkuDetailsWrapper.kt | 61 ++++++++++++------- 2 files changed, 47 insertions(+), 23 deletions(-) create mode 100644 sdk/src/main/java/com/apphud/sdk/internal/BaseAsyncWrapper.kt diff --git a/sdk/src/main/java/com/apphud/sdk/internal/BaseAsyncWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/BaseAsyncWrapper.kt new file mode 100644 index 00000000..f319ce01 --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/internal/BaseAsyncWrapper.kt @@ -0,0 +1,9 @@ +package com.apphud.sdk.internal + +import java.io.Closeable + +abstract class BaseAsyncWrapper : Closeable { + val retryCapacity: Int = 10 + var retryCount: Int = 0 + var retryDelay: Long = 200 +} \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt index 3c252042..8c74c1e5 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt @@ -9,7 +9,7 @@ import com.apphud.sdk.ProductId import com.apphud.sdk.domain.PurchaseRecordDetails import com.apphud.sdk.isSuccess import com.apphud.sdk.logMessage -import java.io.Closeable +import kotlin.concurrent.thread typealias SkuType = String typealias ApphudSkuDetailsCallback = (List) -> Unit @@ -17,48 +17,63 @@ typealias ApphudSkuDetailsRestoreCallback = (List) -> Uni internal class SkuDetailsWrapper( private val billing: BillingClient -) : Closeable { - +) : BaseAsyncWrapper() { var callback: ApphudSkuDetailsCallback? = null var restoreCallback: ApphudSkuDetailsRestoreCallback? = null fun restoreAsync(@BillingClient.SkuType type: SkuType, records: List) { - val products = records.map { it.sku } val params = SkuDetailsParams.newBuilder() .setSkusList(products) .setType(type) .build() - billing.querySkuDetailsAsync(params) { result, details -> - when (result.isSuccess()) { - true -> { - val values = details ?: emptyList() - val purchases = values.map { detail -> - PurchaseRecordDetails( - record = records.first { it.sku == detail.sku }, - details = detail - ) - } - when (purchases.isEmpty()) { - true -> ApphudLog.log("SkuDetails return empty list for $type and records: $records") - else -> restoreCallback?.invoke(purchases) + + thread(start = true, name = "restoreAsync+$type") { + while (!billing.isReady) { + ApphudLog.log("restoreAsync is on waiting for ${retryDelay}ms for $type") + Thread.sleep(retryDelay) + if(retryCount++>=retryCapacity) + break + } + billing.querySkuDetailsAsync(params) { result, details -> + when (result.isSuccess()) { + true -> { + val values = details ?: emptyList() + val purchases = values.map { detail -> + PurchaseRecordDetails( + record = records.first { it.sku == detail.sku }, + details = detail + ) + } + when (purchases.isEmpty()) { + true -> ApphudLog.log("SkuDetails return empty list for $type and records: $records") + else -> restoreCallback?.invoke(purchases) + } } + else -> result.logMessage("restoreAsync type: $type products: $products") } - else -> result.logMessage("restoreAsync type: $type products: $products") } } } fun queryAsync(@BillingClient.SkuType type: SkuType, products: List) { - val params = SkuDetailsParams.newBuilder() .setSkusList(products) .setType(type) .build() - billing.querySkuDetailsAsync(params) { result, details -> - when (result.isSuccess()) { - true -> callback?.invoke(details ?: emptyList()) - else -> result.logMessage("queryAsync type: $type products: $products") + + thread(start = true, name = "queryAsync+$type") { + while (!billing.isReady) { + ApphudLog.log("queryAsync is on waiting for ${retryDelay}ms for $type") + Thread.sleep(retryDelay) + if(retryCount++>=retryCapacity) + break + } + billing.querySkuDetailsAsync(params) { result, details -> + when (result.isSuccess()) { + true -> callback?.invoke(details ?: emptyList()) + else -> result.logMessage("queryAsync type: $type products: $products") + } } } } From e6bbe46fc1910ac2c5ef5cda11d2a794f0849a1d Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Tue, 23 Feb 2021 18:19:12 +0300 Subject: [PATCH 06/11] APH-425 - Callback method for Products were implemented. --- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 12 ++++++++++++ sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 2ad38e1f..47a2c498 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -152,6 +152,18 @@ object Apphud { return ApphudInternal.getSkuDetailsList() } + /** + * This callback is called when SKProducts are fetched from Google Play Billing. + * Note that you have to add all product identifiers in Apphud. + * You can use `productsDidFetchCallback` callback + * or implement `apphudFetchSkuDetailsProducts` listener method. Use whatever you like most. + */ + + @kotlin.jvm.JvmStatic + fun productsFetchCallback(callback: (List) -> Unit) { + ApphudInternal.productsFetchCallback(callback) + } + /** * Returns **SkuDetails** object by product identifier. * Note that you have to add this product identifier in Apphud. diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index d3671812..53c453e1 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -69,6 +69,8 @@ internal object ApphudInternal { private val skuDetails = mutableListOf() + private var customProductsFetchedBlock : ((List) -> Unit)? = null + private fun loadAdsId() { if (ApphudUtils.adTracking) { AdvertisingTask().execute() @@ -148,6 +150,13 @@ internal object ApphudInternal { ApphudLog.log("End registration" ) } + internal fun productsFetchCallback(callback: (List) -> Unit){ + customProductsFetchedBlock = callback + if(skuDetails.isNotEmpty()) { + customProductsFetchedBlock?.invoke(skuDetails) + } + } + internal fun purchase( activity: Activity, productId: String, @@ -322,6 +331,7 @@ internal object ApphudInternal { prevPurchases.clear() skuDetails.clear() allowIdentifyUser = true + customProductsFetchedBlock = null } private fun fetchProducts() { @@ -329,6 +339,7 @@ internal object ApphudInternal { ApphudLog.log("fetchProducts: details from Google Billing: $details") if (details.isNotEmpty()) { skuDetails.addAll(details) + customProductsFetchedBlock?.invoke(skuDetails) apphudListener?.apphudFetchSkuDetailsProducts(details) } } From 5398ae37f6a5f78aa9ecf97a6ad34a5d67c8e3da Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Thu, 25 Feb 2021 19:10:42 +0300 Subject: [PATCH 07/11] APH-399 - Method "Purchase by id" was improved. If the requested product is not in the skuDetails array, then the SkuDetails are loaded from the Google Billing. Only after that the purchase will be made --- .../java/com/apphud/sdk/ApphudInternal.kt | 13 ++++++++++++- .../com/apphud/sdk/internal/BillingWrapper.kt | 9 +++++++-- .../apphud/sdk/internal/SkuDetailsWrapper.kt | 19 +++++++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 53c453e1..74611966 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -166,7 +166,18 @@ internal object ApphudInternal { if (sku != null) { purchase(activity, sku, callback) } else { - ApphudLog.log("could not fetch sku details for product id: $productId") + ApphudLog.log("Could not find SkuDetails for product id: $productId in memory") + ApphudLog.log("Now try fetch it from Google Billing") + billing.details(BillingClient.SkuType.SUBS, listOf(productId)) { skuList -> + ApphudLog.log("Google Billing (SUBS) return this info for product id = $productId :") + skuList.forEach { ApphudLog.log("$it") } + skuList.takeIf { it.isNotEmpty() }?.let { skuDetails.addAll(it); purchase(activity, it.first() , callback) } + } + billing.details(BillingClient.SkuType.INAPP, listOf(productId)) { skuList -> + ApphudLog.log("Google Billing (SUBS) return this info for product id = $productId :") + skuList.forEach { ApphudLog.log("$it") } + skuList.takeIf { it.isNotEmpty() }?.let { skuDetails.addAll(it); purchase(activity, it.first() , callback) } + } } } diff --git a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt index 2f79c184..8fb4304e 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt @@ -43,7 +43,7 @@ internal class BillingWrapper(context: Context) : BillingClientStateListener, Cl var skuCallback: ApphudSkuDetailsCallback? = null set(value) { field = value - sku.callback = value + sku.detailsCallback = value } var restoreCallback: ApphudSkuDetailsRestoreCallback? = null @@ -87,7 +87,12 @@ internal class BillingWrapper(context: Context) : BillingClientStateListener, Cl } fun details(@BillingClient.SkuType type: SkuType, products: List) = - sku.queryAsync(type, products) + details(type = type, products = products, manualCallback = null) + + fun details(@BillingClient.SkuType type: SkuType, + products: List, + manualCallback: ApphudSkuDetailsCallback? = null) = + sku.queryAsync(type = type, products = products, manualCallback = manualCallback) fun restore(@BillingClient.SkuType type: SkuType, products: List) = sku.restoreAsync(type, products) diff --git a/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt index 8c74c1e5..e5ae74d5 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt @@ -18,7 +18,7 @@ typealias ApphudSkuDetailsRestoreCallback = (List) -> Uni internal class SkuDetailsWrapper( private val billing: BillingClient ) : BaseAsyncWrapper() { - var callback: ApphudSkuDetailsCallback? = null + var detailsCallback: ApphudSkuDetailsCallback? = null var restoreCallback: ApphudSkuDetailsRestoreCallback? = null fun restoreAsync(@BillingClient.SkuType type: SkuType, records: List) { @@ -56,7 +56,15 @@ internal class SkuDetailsWrapper( } } - fun queryAsync(@BillingClient.SkuType type: SkuType, products: List) { + /** + * This function will return SkuDetails according to the requested product list. + * If manualCallback was defined then the result will be moved to this callback, otherwise detailsCallback will be used + * */ + fun queryAsync( + @BillingClient.SkuType type: SkuType, + products: List, + manualCallback: ApphudSkuDetailsCallback? = null + ) { val params = SkuDetailsParams.newBuilder() .setSkusList(products) .setType(type) @@ -71,7 +79,10 @@ internal class SkuDetailsWrapper( } billing.querySkuDetailsAsync(params) { result, details -> when (result.isSuccess()) { - true -> callback?.invoke(details ?: emptyList()) + true -> { + manualCallback?.let{ manualCallback.invoke(details.orEmpty()) } ?: + detailsCallback?.invoke(details.orEmpty()) + } else -> result.logMessage("queryAsync type: $type products: $products") } } @@ -80,7 +91,7 @@ internal class SkuDetailsWrapper( //Closeable override fun close() { - callback = null + detailsCallback = null restoreCallback = null } } \ No newline at end of file From a15fc1a4f8006cfd369d00f3e28ba4c726e923be Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Fri, 26 Feb 2021 17:52:16 +0300 Subject: [PATCH 08/11] APH-xxx - HotFixes to improve code quality and stability --- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 17 ----------------- .../main/java/com/apphud/sdk/ApphudInternal.kt | 4 +--- .../com/apphud/sdk/tasks/AdvertisingTask.kt | 3 +++ 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 47a2c498..71784380 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -29,23 +29,6 @@ object Apphud { fun start(context: Context, apiKey: ApiKey, userId: UserId? = null) = start(context, apiKey, userId, null) -/* private fun isMainProcess(context: Context): Boolean = - context.packageName == getProcessName(context) - - private fun getProcessName(context: Context): String? { - val mypid = Process.myPid() - val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? - return manager?.let { - val infos = manager.runningAppProcesses - for (info in infos) { - if (info.pid == mypid) { - return@let info.processName - } - } - null - } - }*/ - /** * Initializes Apphud SDK. You should call it during app launch. * diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 74611966..0fc813cc 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -50,7 +50,7 @@ internal object ApphudInternal { ApphudLog.log("advertisingId = $advertisingId is fetched and saved") } ApphudLog.log("advertisingId: continue registration") - updateRegistration() + registration(userId, deviceId) } private var allowIdentifyUser = true @@ -382,8 +382,6 @@ internal object ApphudInternal { return deviceId } - private fun updateRegistration() = registration(userId, deviceId) - private fun mkPurchasesBody(purchases: List) = PurchaseBody( device_id = deviceId, diff --git a/sdk/src/main/java/com/apphud/sdk/tasks/AdvertisingTask.kt b/sdk/src/main/java/com/apphud/sdk/tasks/AdvertisingTask.kt index 100a54c0..bd009a01 100644 --- a/sdk/src/main/java/com/apphud/sdk/tasks/AdvertisingTask.kt +++ b/sdk/src/main/java/com/apphud/sdk/tasks/AdvertisingTask.kt @@ -15,6 +15,9 @@ fun advertisingId(context: Context): String? = try { } catch (e: IOException) { ApphudLog.log("finish load advertisingId $e") null +} catch (e: IllegalStateException) { + ApphudLog.log("finish load advertisingId $e") + null } catch (e: GooglePlayServicesNotAvailableException) { ApphudLog.log("finish load advertisingId $e") null From 13012ddd9f44ec5813d0a3c930f0ca07cc2429a8 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Mon, 1 Mar 2021 19:45:18 +0300 Subject: [PATCH 09/11] APH-402 New Feature "userProperties" was added --- .../java/com/apphud/sdk/ApphudServiceTest.kt | 46 +++++++++- .../androidTest/java/com/apphud/sdk/data.kt | 4 +- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 57 +++++++++++-- .../java/com/apphud/sdk/ApphudInternal.kt | 85 ++++++++++++++++++- .../java/com/apphud/sdk/ApphudUserProperty.kt | 36 ++++++++ .../com/apphud/sdk/ApphudUserPropertyKey.kt | 37 ++++++++ .../com/apphud/sdk/body/UserPropertiesBody.kt | 6 ++ .../com/apphud/sdk/client/ApphudClient.kt | 15 +++- .../com/apphud/sdk/client/ApphudService.kt | 19 ++++- .../sdk/tasks/UserPropertiesCallable.kt | 20 +++++ 10 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt create mode 100644 sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt create mode 100644 sdk/src/main/java/com/apphud/sdk/body/UserPropertiesBody.kt create mode 100644 sdk/src/main/java/com/apphud/sdk/tasks/UserPropertiesCallable.kt diff --git a/sdk/src/androidTest/java/com/apphud/sdk/ApphudServiceTest.kt b/sdk/src/androidTest/java/com/apphud/sdk/ApphudServiceTest.kt index 017fe8f2..4d4c5221 100644 --- a/sdk/src/androidTest/java/com/apphud/sdk/ApphudServiceTest.kt +++ b/sdk/src/androidTest/java/com/apphud/sdk/ApphudServiceTest.kt @@ -1,10 +1,7 @@ package com.apphud.sdk import android.util.Log -import com.apphud.sdk.body.AttributionBody -import com.apphud.sdk.body.PurchaseBody -import com.apphud.sdk.body.PurchaseItemBody -import com.apphud.sdk.body.PushBody +import com.apphud.sdk.body.* import com.apphud.sdk.client.ApiClient import com.apphud.sdk.client.ApphudService import com.apphud.sdk.client.HttpUrlConnectionExecutor @@ -108,4 +105,45 @@ class ApphudServiceTest { val response = service.purchase(body) Log.e("WOW", "send push result: ${response.data.results}") } + + @Test + fun userPropertiesTest() { + val body = UserPropertiesBody( + device_id = deviceId, + properties = listOf( + mapOf( + "set_once" to true, + "kind" to "string", + "value" to "user4@example.com", + "name" to "\$email" + ), + mapOf( + "kind" to "integer", + "set_once" to false, + "name" to "\$age", + "value" to 31 + ), + mapOf( + "set_once" to false, + "value" to true, + "name" to "custom_test_property_1", + "kind" to "boolean" + ), + mapOf( + "set_once" to false, + "value" to "gay", + "name" to "\$gender", + "kind" to "string" + ), + mapOf( + "name" to "custom_email", + "value" to "user2@example.com", + "kind" to "string", + "set_once" to true + ) + ) + ) + val response = service.sendUserProperties(body) + Log.e("WOW", "send user properties result: ${response.data.results}") + } } \ No newline at end of file diff --git a/sdk/src/androidTest/java/com/apphud/sdk/data.kt b/sdk/src/androidTest/java/com/apphud/sdk/data.kt index c9cbe360..5f32c687 100644 --- a/sdk/src/androidTest/java/com/apphud/sdk/data.kt +++ b/sdk/src/androidTest/java/com/apphud/sdk/data.kt @@ -16,5 +16,7 @@ internal fun mkRegistrationBody(userId: String, deviceId: String) = idfa = "22221111", user_id = userId, device_id = deviceId, - time_zone = "UTF" + time_zone = "UTF", + is_sandbox = true, + is_new = true ) \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 71784380..2f0ea750 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -153,15 +153,15 @@ object Apphud { * Will return **null** if product is not yet fetched from Google Play Billing. */ @kotlin.jvm.JvmStatic - fun products(productIdentifier: String): SkuDetails? { + fun product(productIdentifier: String): SkuDetails? { return ApphudInternal.getSkuDetailsByProductId(productIdentifier) } /** * Purchases product and automatically submit - * @activity: current Activity for use - * @productId: The identifier of the product you wish to purchase - * @block: The closure that will be called when purchase completes. + * @param activity: current Activity for use + * @param productId: The identifier of the product you wish to purchase + * @param block: The closure that will be called when purchase completes. */ @kotlin.jvm.JvmStatic fun purchase(activity: Activity, productId: String, block: (List) -> Unit) = @@ -169,14 +169,57 @@ object Apphud { /** * Purchases product and automatically submit - * @activity: current Activity for use - * @details: The skuDetails of the product you wish to purchase - * @block: The closure that will be called when purchase completes. + * @param activity current Activity for use + * @param details The skuDetails of the product you wish to purchase + * @param block The closure that will be called when purchase completes. */ @kotlin.jvm.JvmStatic fun purchase(activity: Activity, details: SkuDetails, block: (List) -> Unit) = ApphudInternal.purchase(activity, details, block) + + /** + * Set custom user property. + * Value must be one of: "Int", "Float", "Double", "Boolean", "String" or "null". + * + * Example: + * // use built-in property key + * Apphud.setUserProperty(key: ApphudUserPropertyKey.Email, value: "user4@example.com", setOnce: true) + * // use custom property key + * Apphud.setUserProperty(key: ApphudUserPropertyKey.CustomProperty("custom_test_property_1"), value: 0.5) + * + * __Note__: You can use several built-in keys with their value types: + * "ApphudUserPropertyKey.Email": User email. Value must be String. + * "ApphudUserPropertyKey.Name": User name. Value must be String. + * "ApphudUserPropertyKey.Phone": User phone number. Value must be String. + * "ApphudUserPropertyKey.Age": User age. Value must be Int. + * "ApphudUserPropertyKey.Gender": User gender. Value must be one of: "male", "female", "other". + * "ApphudUserPropertyKey.Cohort": User install cohort. Value must be String. + * + * @param key Required. Initialize class with custom string or using built-in keys. See example above. + * @param value Required/Optional. Pass "null" to remove given property from Apphud. + * @param setOnce Optional. Pass "true" to make this property non-updatable. + */ + @kotlin.jvm.JvmStatic + fun setUserProperty(key: ApphudUserPropertyKey, value: Any?, setOnce: Boolean = false) { + ApphudInternal.setUserProperty(key = key, value = value, setOnce = setOnce, increment = false) + } + + /** + * Increment custom user property. + * Value must be one of: "Int", "Float", "Double". + * + * Example: + * Apphud.incrementUserProperty(key: ApphudUserPropertyKey.CustomProperty("progress"), by: 0.5) + * + * @param key Required. Use your custom string key or some of built-in keys. + * @param by Required/Optional. You can pass negative value to decrement. + */ + @kotlin.jvm.JvmStatic + fun incrementUserProperty(key: ApphudUserPropertyKey, by: Any) { + ApphudInternal.setUserProperty(key = key, value = by, setOnce = false, increment = true) + } + /** * Enables debug logs. Better to call this method before SDK initialization. */ diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 0fc813cc..81c1bd8b 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -10,10 +10,7 @@ import android.os.Looper import com.android.billingclient.api.BillingClient import com.android.billingclient.api.Purchase import com.android.billingclient.api.SkuDetails -import com.apphud.sdk.body.AttributionBody -import com.apphud.sdk.body.PurchaseBody -import com.apphud.sdk.body.PurchaseItemBody -import com.apphud.sdk.body.RegistrationBody +import com.apphud.sdk.body.* import com.apphud.sdk.client.ApphudClient import com.apphud.sdk.domain.* import com.apphud.sdk.internal.BillingWrapper @@ -71,6 +68,20 @@ internal object ApphudInternal { private var customProductsFetchedBlock : ((List) -> Unit)? = null + private val pendingUserProperties = mutableMapOf() + private val userPropertiesRunnable = Runnable { updateUserProperties() } + + private var setNeedsToUpdateUserProperties: Boolean = false + set(value) { + field = value + if (value) { + handler.removeCallbacks(userPropertiesRunnable) + handler.postDelayed(userPropertiesRunnable, 1000L) + } else { + handler.removeCallbacks(userPropertiesRunnable) + } + } + private fun loadAdsId() { if (ApphudUtils.adTracking) { AdvertisingTask().execute() @@ -329,6 +340,70 @@ internal object ApphudInternal { } } + internal fun setUserProperty( + key: ApphudUserPropertyKey, + value: Any?, + setOnce: Boolean, + increment: Boolean + ) { + val typeString = getType(value) + if (typeString == "unknown") { + val type = value?.let { value::class.java.name } ?: "unknown" + ApphudLog.log("For key '${key.key}' invalid property type: '$type' for 'value'. Must be one of: [Int, Float, Double, Boolean, String or null]") + return + } + if (increment && !(typeString == "integer" || typeString == "float")) { + val type = value?.let { value::class.java.name } ?: "unknown" + ApphudLog.log("For key '${key.key}' invalid increment property type: '$type' for 'value'. Must be one of: [Int, Float or Double]") + return + } + + val property = ApphudUserProperty(key = key.key, + value = value, + increment = increment, + setOnce = setOnce, + type = typeString) + + pendingUserProperties.run { + remove(property.key) + put(property.key, property) + } + setNeedsToUpdateUserProperties = true + } + + private fun updateUserProperties() { + setNeedsToUpdateUserProperties = false + if (pendingUserProperties.isEmpty()) return + + val properties = mutableListOf>() + pendingUserProperties.forEach { + properties.add(it.value.toJSON()!!) + } + + val body = UserPropertiesBody(this.deviceId, properties) + client.userProperties(body) { userproperties -> + handler.post { + if (userproperties.success) { + pendingUserProperties.clear() + ApphudLog.log("User Properties successfully updated.") + } else { + ApphudLog.log("User Properties update failed with this errors") + } + } + } + } + + private fun getType(value: Any?): String { + return when (value) { + is String -> "string" + is Boolean -> "boolean" + is Float, Double -> "float" + is Int -> "integer" + null -> "null" + else -> "unknown" + } + } + internal fun logout() { clear() } @@ -343,6 +418,8 @@ internal object ApphudInternal { skuDetails.clear() allowIdentifyUser = true customProductsFetchedBlock = null + pendingUserProperties.clear() + setNeedsToUpdateUserProperties = false } private fun fetchProducts() { diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt b/sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt new file mode 100644 index 00000000..2c727f2b --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt @@ -0,0 +1,36 @@ +package com.apphud.sdk + +internal const val JSON_NAME_KEY = "key" +internal const val JSON_NAME_VALUE = "value" +internal const val JSON_NAME_SET_ONCE = "set_once" +internal const val JSON_NAME_KIND = "kind" +internal const val JSON_NAME_INCREMENT = "increment" + +internal data class ApphudUserProperty( + val key: String, + val value: Any?, + val increment: Boolean = false, + val setOnce: Boolean = false, + val type: String = "" +) { + + fun toJSON(): MutableMap? { + if (increment && value == null) { + return null + } + + val jsonParamsString: MutableMap = mutableMapOf( + JSON_NAME_KEY to key, + JSON_NAME_VALUE to if (value !is Float || value !is Double) value else value as Double, + JSON_NAME_SET_ONCE to setOnce + ) + if (value != null) { + jsonParamsString[JSON_NAME_KIND] = type + } + if (increment) { + jsonParamsString[JSON_NAME_INCREMENT] = increment + } + return jsonParamsString + } + +} diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt b/sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt new file mode 100644 index 00000000..e80c37a5 --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt @@ -0,0 +1,37 @@ +package com.apphud.sdk + +/** +Built-in property keys. + */ +/** User email. Value must be String. */ +internal const val ApphudUserPropertyKeyEmail = "\$email" + +/** User name. Value must be String. */ +internal const val ApphudUserPropertyKeyName = "\$name" + +/** User phone number. Value must be String. */ +internal const val ApphudUserPropertyKeyPhone = "\$phone" + +/** User install cohort. Value must be String. */ +internal const val ApphudUserPropertyKeyCohort = "\$cohort" + +/** User email. Value must be Int. */ +internal const val ApphudUserPropertyKeyAge = "\$age" + +/** User email. Value must be one of: "male", "female", "other". */ +internal const val ApphudUserPropertyKeyGender = "\$gender" + +sealed class ApphudUserPropertyKey(val key: String){ + object Email:ApphudUserPropertyKey(ApphudUserPropertyKeyEmail) + object Name:ApphudUserPropertyKey(ApphudUserPropertyKeyName) + object Phone:ApphudUserPropertyKey(ApphudUserPropertyKeyPhone) + object Cohort:ApphudUserPropertyKey(ApphudUserPropertyKeyCohort) + object Age:ApphudUserPropertyKey(ApphudUserPropertyKeyAge) + object Gender:ApphudUserPropertyKey(ApphudUserPropertyKeyGender) + /** + Initialize with custom property key string. + Example: + Apphud.setUserProperty(key = ApphudUserPropertyKey.CustomProperty("custom_prop_1"), value = 0.5) + */ + class CustomProperty(value: String):ApphudUserPropertyKey(value) +} \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/body/UserPropertiesBody.kt b/sdk/src/main/java/com/apphud/sdk/body/UserPropertiesBody.kt new file mode 100644 index 00000000..83f5b64b --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/body/UserPropertiesBody.kt @@ -0,0 +1,6 @@ +package com.apphud.sdk.body + +data class UserPropertiesBody( + val device_id: String, + val properties: List> +) \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt b/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt index 8efc2a25..e39b9063 100644 --- a/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt +++ b/sdk/src/main/java/com/apphud/sdk/client/ApphudClient.kt @@ -1,10 +1,7 @@ package com.apphud.sdk.client import com.apphud.sdk.* -import com.apphud.sdk.body.AttributionBody -import com.apphud.sdk.body.PurchaseBody -import com.apphud.sdk.body.PushBody -import com.apphud.sdk.body.RegistrationBody +import com.apphud.sdk.body.* import com.apphud.sdk.mappers.AttributionMapper import com.apphud.sdk.mappers.CustomerMapper import com.apphud.sdk.mappers.ProductMapper @@ -72,4 +69,14 @@ internal class ApphudClient(apiKey: ApiKey, parser: Parser) { } }) } + + fun userProperties(body: UserPropertiesBody, callback: AttributionCallback) { + val callable = UserPropertiesCallable(body, service) + thread.execute(LoopRunnable(callable) { response -> + when (response.data.results) { + null -> ApphudLog.log("Response success but result is null") + else -> callback.invoke(attributionMapper.map(response.data.results)) + } + }) + } } \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/client/ApphudService.kt b/sdk/src/main/java/com/apphud/sdk/client/ApphudService.kt index 4213b1db..3d159df7 100644 --- a/sdk/src/main/java/com/apphud/sdk/client/ApphudService.kt +++ b/sdk/src/main/java/com/apphud/sdk/client/ApphudService.kt @@ -1,10 +1,7 @@ package com.apphud.sdk.client import com.apphud.sdk.ApiKey -import com.apphud.sdk.body.AttributionBody -import com.apphud.sdk.body.PurchaseBody -import com.apphud.sdk.body.PushBody -import com.apphud.sdk.body.RegistrationBody +import com.apphud.sdk.body.* import com.google.gson.reflect.TypeToken import com.apphud.sdk.client.dto.* @@ -89,4 +86,18 @@ class ApphudService( ), body ) + + /** + * Отправка user property + */ + fun sendUserProperties(body: UserPropertiesBody): ResponseDto = + executor.call( + RequestConfig( + path = "customers/properties", + type = object : TypeToken>(){}.type, + queries = mapOf(API_KEY to apiKey), + requestType = RequestType.POST + ), + body + ) } \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/tasks/UserPropertiesCallable.kt b/sdk/src/main/java/com/apphud/sdk/tasks/UserPropertiesCallable.kt new file mode 100644 index 00000000..fbec5058 --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/tasks/UserPropertiesCallable.kt @@ -0,0 +1,20 @@ +package com.apphud.sdk.tasks + +import com.apphud.sdk.body.UserPropertiesBody +import com.apphud.sdk.client.ApphudService +import com.apphud.sdk.client.dto.AttributionDto +import com.apphud.sdk.client.dto.ResponseDto + +internal class UserPropertiesCallable( + private val body: UserPropertiesBody, + private val service: ApphudService +) : PriorityCallable> { + override val priority: Int = Int.MAX_VALUE + override fun call(): ResponseDto = service.sendUserProperties(body) + private var _counter: Int = 0 + override var counter: Int + get() = _counter + set(value) { + _counter = value + } +} \ No newline at end of file From 8acd33cee3dbaf4991dde19c1a0902a26a5222f9 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Tue, 2 Mar 2021 20:04:39 +0300 Subject: [PATCH 10/11] APH-402 Hot fixes for function "userProperties" --- sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt | 4 ++-- sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt b/sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt index 2c727f2b..a0ce4c02 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudUserProperty.kt @@ -1,6 +1,6 @@ package com.apphud.sdk -internal const val JSON_NAME_KEY = "key" +internal const val JSON_NAME_NAME = "name" internal const val JSON_NAME_VALUE = "value" internal const val JSON_NAME_SET_ONCE = "set_once" internal const val JSON_NAME_KIND = "kind" @@ -20,7 +20,7 @@ internal data class ApphudUserProperty( } val jsonParamsString: MutableMap = mutableMapOf( - JSON_NAME_KEY to key, + JSON_NAME_NAME to key, JSON_NAME_VALUE to if (value !is Float || value !is Double) value else value as Double, JSON_NAME_SET_ONCE to setOnce ) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt b/sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt index e80c37a5..b8cf2e96 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudUserPropertyKey.kt @@ -22,11 +22,17 @@ internal const val ApphudUserPropertyKeyAge = "\$age" internal const val ApphudUserPropertyKeyGender = "\$gender" sealed class ApphudUserPropertyKey(val key: String){ + /** User email. Value must be String*/ object Email:ApphudUserPropertyKey(ApphudUserPropertyKeyEmail) + /** User name. Value must be String*/ object Name:ApphudUserPropertyKey(ApphudUserPropertyKeyName) + /** User phone number. Value must be String.*/ object Phone:ApphudUserPropertyKey(ApphudUserPropertyKeyPhone) + /** User age. Value must be Int.*/ object Cohort:ApphudUserPropertyKey(ApphudUserPropertyKeyCohort) + /** User install cohort. Value must be String.*/ object Age:ApphudUserPropertyKey(ApphudUserPropertyKeyAge) + /** User gender. Value must be one of: "male", "female", "other".*/ object Gender:ApphudUserPropertyKey(ApphudUserPropertyKeyGender) /** Initialize with custom property key string. From 87b43413692265f5bf5f50417cbc90ce94234b2b Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Wed, 3 Mar 2021 07:15:47 +0300 Subject: [PATCH 11/11] APH-402 Fixes for pseudo-asynchronous operation of the "userProperties" method. --- sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 81c1bd8b..e26b0fdb 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -25,6 +25,7 @@ internal object ApphudInternal { private val builder = GsonBuilder() .setPrettyPrinting() + .serializeNulls()//need this to pass nullable values to JSON and from JSON .create() private val parser: Parser = GsonParser(builder) @@ -51,6 +52,7 @@ internal object ApphudInternal { } private var allowIdentifyUser = true + private var isRegistered = false internal var userId: UserId? = null private lateinit var deviceId: DeviceId @@ -69,7 +71,7 @@ internal object ApphudInternal { private var customProductsFetchedBlock : ((List) -> Unit)? = null private val pendingUserProperties = mutableMapOf() - private val userPropertiesRunnable = Runnable { updateUserProperties() } + private val userPropertiesRunnable = Runnable { if(isRegistered) updateUserProperties() } private var setNeedsToUpdateUserProperties: Boolean = false set(value) { @@ -144,6 +146,7 @@ internal object ApphudInternal { val body = mkRegistrationBody(userId!!, this.deviceId) client.registrationUser(body) { customer -> + isRegistered = true handler.post { ApphudLog.log("registration registrationUser customer=${customer.toString()}" ) storage.customer = customer @@ -155,6 +158,11 @@ internal object ApphudInternal { ApphudLog.log("registration syncPurchases" ) syncPurchases() } + + if(pendingUserProperties.isNotEmpty() && setNeedsToUpdateUserProperties) { + ApphudLog.log("registration we should update UserProperties" ) + updateUserProperties() + } } } @@ -409,6 +417,7 @@ internal object ApphudInternal { } private fun clear() { + isRegistered = false storage.customer = null storage.userId = null storage.deviceId = null