Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

Commit

Permalink
Release v1.6.0
Browse files Browse the repository at this point in the history
  • Loading branch information
lgarbo committed Oct 30, 2023
1 parent 27141e0 commit 7457834
Show file tree
Hide file tree
Showing 48 changed files with 1,606 additions and 273 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Add the dependency to your module-level `build.gradle`:
```
dependencies {
[...]
implementation 'io.glassfy:androidsdk:1.5.2'
implementation 'io.glassfy:androidsdk:1.6.0'
}
```

Expand All @@ -33,7 +33,7 @@ Add the dependency to your module-level `build.gradle.kts`:
```
dependencies {
[...]
implementation("io.glassfy:androidsdk:1.5.2")
implementation("io.glassfy:androidsdk:1.6.0")
}
```

Expand Down
4 changes: 2 additions & 2 deletions buildSrc/src/main/java/io/glassfy/androidsdk/Configuration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ object Configuration {
const val targetSdk = 33
const val minSdk = 21
private const val majorVersion = 1
private const val minorVersion = 5
private const val patchVersion = 2
private const val minorVersion = 6
private const val patchVersion = 0
const val versionName = "$majorVersion.$minorVersion.$patchVersion"
const val snapshotVersionName = "${versionName}-SNAPSHOT"
const val artifactGroup = "io.glassfy"
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/src/main/java/io/glassfy/paywall/Configuration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ object Configuration {
const val targetSdk = 33
const val minSdk = 24
private const val majorVersion = 1
private const val minorVersion = 5
private const val patchVersion = 2
private const val minorVersion = 6
private const val patchVersion = 0
const val versionName = "$majorVersion.$minorVersion.$patchVersion"
const val snapshotVersionName = "${versionName}-SNAPSHOT"
const val artifactGroup = "io.glassfy"
Expand Down
11 changes: 5 additions & 6 deletions glassfy/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,16 @@ android {

dependencies {
// Lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.lifecycle:lifecycle-process:2.6.1")

// Android Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")

// BillingClient
implementation("com.android.billingclient:billing-ktx:5.2.1")
implementation("com.android.billingclient:billing-ktx:6.0.1")

// Retrofit + OKHttp + Moshi
// implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.moshi:moshi:1.15.0")
Expand All @@ -81,8 +80,8 @@ dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")

androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

// Sources
Expand Down
11 changes: 0 additions & 11 deletions glassfy/src/main/java/io/glassfy/androidsdk/Glassfy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,6 @@ object Glassfy {
customScope.runAndPostResult(callback) { manager.skubase(identifier, store) }
}

/**
* Fetch Sku
*
* @param identifier Store product identifier
* @param callback Completion callback with results
*/
@JvmStatic
fun skuWithProductId(identifier: String, callback: SkuCallback) {
customScope.runAndPostResult(callback) { manager.skuWithProductId(identifier) }
}

/**
* Chek permissions status of the user
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ enum class GlassfyErrorCode(internal val internalCode: Int? = null) {
SDKNotInitialized,
MissingPurchase,
PendingPurchase,
Purchasing,
Purchasing(-199),

StoreError,
UserCancelPurchase,
Expand Down
163 changes: 93 additions & 70 deletions glassfy/src/main/java/io/glassfy/androidsdk/internal/GManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.Purchase.PurchaseState.PURCHASED
import com.squareup.moshi.Moshi
import io.glassfy.androidsdk.*
import io.glassfy.androidsdk.Glassfy
import io.glassfy.androidsdk.GlassfyErrorCode
import io.glassfy.androidsdk.LogLevel
import io.glassfy.androidsdk.PurchaseDelegate
import io.glassfy.androidsdk.internal.billing.IBillingPurchaseDelegate
import io.glassfy.androidsdk.internal.billing.IBillingService
import io.glassfy.androidsdk.internal.billing.google.PlayBillingService
import io.glassfy.androidsdk.internal.billing.SkuDetailsQuery
import io.glassfy.androidsdk.internal.billing.play.PlayBillingServiceProvider
import io.glassfy.androidsdk.internal.cache.CacheManager
import io.glassfy.androidsdk.internal.cache.ICacheManager
import io.glassfy.androidsdk.internal.device.DeviceManager
Expand Down Expand Up @@ -113,7 +116,7 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {
val deviceManager: IDeviceManager = DeviceManager(appContext.applicationContext)
val apiService: IApiService = makeApiService(cacheManager, deviceManager, apiKey)
repository = Repository(apiService)
billingService = PlayBillingService(this, appContext, watcherMode)
billingService = PlayBillingServiceProvider.billingService(this, appContext, watcherMode)

val res = _initialize(crossPlatformSdkFramework, crossPlatformSdkVersion)
if (res.err != null) {
Expand Down Expand Up @@ -151,14 +154,11 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {
internal suspend fun restore(): Resource<Permissions> = withSdkInitializedOrError { _restore() }

internal suspend fun sku(identifier: String): Resource<Sku> =
withSdkInitializedOrError { _playstoresku(identifier) }
withSdkInitializedOrError { _playStoreSku(identifier) }

internal suspend fun skubase(identifier: String, store: Store): Resource<out ISkuBase> =
withSdkInitializedOrError { _skubase(identifier, store) }

internal suspend fun skuWithProductId(identifier: String): Resource<Sku> =
withSdkInitializedOrError { _skuWithProductId(identifier) }

internal suspend fun offerings(): Resource<Offerings> =
withSdkInitializedOrError { _offerings() }

Expand Down Expand Up @@ -281,51 +281,93 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {
}

private suspend fun _offerings(): Resource<Offerings> {
val offRes = repository.offerings()
val offRes = repository.offerings(billingService.version)
if (offRes.err != null) return offRes
if (offRes.data == null) return Resource.Error(GlassfyErrorCode.NotFoundOnGlassfy.toError())

val detailRes = offRes.data.all.flatMap { it.skus.map { s -> s.productId } }.toSet()
.let { billingService.skuDetails(it) }
if (detailRes.err != null) return Resource.Error(detailRes.err)
if (detailRes.data == null) return Resource.Error(GlassfyErrorCode.NotFoundOnStore.toError())
val queries =
offRes.data.all.flatMap { off -> SkuDetailsQuery.fromSkus(off.skus) }.distinct()

val detailRes = billingService.skusDetails(queries).data ?: emptyList()
for (o in offRes.data.all) {
o.skus_ = o.skus.filter { sku -> detailRes.data.map { it.sku }.contains(sku.productId) }
.map { s ->
detailRes.data.find { detail -> detail.sku == s.productId }
?.let { detail -> s.product = detail }
return@map s
o.skus_ = o.skus.mapNotNull { s ->
matchSkuWithStoreDetails(s, detailRes)?.let {
s.apply {
product = it
}
}
}
}
return Resource.Success(offRes.data)
}

private fun matchSkuWithStoreDetails(s: Sku, storeDetails: List<SkuDetails>) =
matchSkuWithStoreDetailsAndFallback(s, storeDetails)

private fun matchSkuWithStoreDetailsAndFallback(
s: Sku, storeDetails: List<SkuDetails>
): SkuDetails? {
return storeDetails.find {
it.sku == s.skuParams.productId && it.basePlanId == s.skuParams.basePlanId.orEmpty() && it.offerId == s.skuParams.offerId.orEmpty()
}?.also {
Logger.logDebug(
"Sku Found ${s.skuId}: " + "\t${s.skuParams.productId} - ${s.skuParams.basePlanId} - ${s.skuParams.offerId}"
)
} ?: storeDetails.find {
it.sku == s.fallbackSkuParams?.productId && it.basePlanId == s.fallbackSkuParams.basePlanId.orEmpty() && it.offerId == s.fallbackSkuParams.offerId.orEmpty()
}.also {
if (it == null) {
Logger.logDebug(
"Sku NOT Found ${s.skuId}: " + "\t${s.skuParams.productId} - ${s.skuParams.basePlanId} - ${s.skuParams.offerId}"
)
} else {
Logger.logDebug(
"Sku Fallback ${s.skuId}: " + "\n\t${s.skuParams.productId} - ${s.skuParams.basePlanId} - ${s.skuParams.offerId}" + "\n\t" +
"${it.sku} - ${it.basePlanId.ifEmpty { null }} - ${it.offerId.ifEmpty { null }}"
)
}
}
return offRes
}

private suspend fun _purchase(
activity: Activity, sku: Sku, upgradeSku: SubscriptionUpdate?, accountId: String?
): Resource<Transaction> {
if (upgradeSku != null) {
val purchases = billingService.allPurchases()
if (purchases.err != null) return Resource.Error(purchases.err)

val res = repository.skuByIdentifier(upgradeSku.originalSku)
if (res.err != null) return Resource.Error(res.err)

val purchases = billingService.allPurchases()
if (purchases.err != null) return Resource.Error(purchases.err)
purchases.data?.firstOrNull { it.skus.contains(res.data?.productId ?: "") }
?.let { upgradeSku.purchaseToken = it.purchaseToken }
listOfNotNull(
res.data?.skuParams?.productId, res.data?.fallbackSkuParams?.productId
).firstNotNullOfOrNull { purchasedProduct ->
purchases.data?.firstOrNull { purchase ->
purchase.skus.contains(purchasedProduct)
}
}?.also { purchase ->
upgradeSku.purchaseToken = purchase.purchaseToken
}

if (upgradeSku.purchaseToken.isEmpty()) {
return Resource.Error(GlassfyErrorCode.MissingPurchase.toError("purchaseToken not found for ${upgradeSku.originalSku}"))
return Resource.Error(
GlassfyErrorCode.MissingPurchase.toError(
"purchaseToken not found for ${upgradeSku.originalSku}"
)
)
}
}

val result = billingService.purchase(activity, sku.product, upgradeSku, accountId)
val result = billingService.purchase(
activity, sku.product, upgradeSku, accountId
)
if (result.err != null) return Resource.Error(result.err)
if (result.data == null) return Resource.Error(GlassfyErrorCode.MissingPurchase.toError())
if (result.data.purchaseState != PURCHASED) return Resource.Error(GlassfyErrorCode.PendingPurchase.toError())

return result.data.let { p ->
val tokenReq = TokenRequest.from(p, sku.product.type == BillingClient.SkuType.SUBS)
tokenReq.offeringId = sku.offeringId
val tokenReq = TokenRequest.from(p, sku.isSubscription(), sku.offeringId, sku.product)

repository.token(tokenReq).apply {
data?.permissions?.installationId_ = cacheManager.installationId
}
Expand All @@ -346,7 +388,7 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {

private suspend fun _skubase(identifier: String, store: Store): Resource<out ISkuBase> {
if (store == Store.PlayStore) {
return _playstoresku(identifier)
return _playStoreSku(identifier)
}

val skuRes = repository.skuByIdentifierAndStore(identifier, store)
Expand All @@ -355,40 +397,19 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {
return Resource.Success(skuRes.data)
}

private suspend fun _playstoresku(identifier: String): Resource<Sku> {
private suspend fun _playStoreSku(identifier: String): Resource<Sku> {
val skuRes = repository.skuByIdentifier(identifier)
if (skuRes.err != null) return skuRes
if (skuRes.data == null) return Resource.Error(GlassfyErrorCode.NotFoundOnGlassfy.toError())

return skuRes.data.let {
val detailRes = billingService.skuDetails(setOf(it.productId))
if (detailRes.err != null) return Resource.Error(detailRes.err)
if (detailRes.data.isNullOrEmpty()) return Resource.Error(
GlassfyErrorCode.NotFoundOnStore.toError()
)

Resource.Success(it.apply {
product = detailRes.data.first()
})
}
}

private suspend fun _skuWithProductId(identifier: String): Resource<Sku> {
val skuRes = repository.skuByProductId(identifier)
if (skuRes.err != null) return skuRes
if (skuRes.data == null) return Resource.Error(GlassfyErrorCode.NotFoundOnGlassfy.toError())

return skuRes.data.let {
val detailRes = billingService.skuDetails(setOf(it.productId))
if (detailRes.err != null) return Resource.Error(detailRes.err)
if (detailRes.data.isNullOrEmpty()) return Resource.Error(
GlassfyErrorCode.NotFoundOnStore.toError()
)
val detailRes = billingService.skusDetails(SkuDetailsQuery.fromSku(skuRes.data))
if (detailRes.err != null) return Resource.Error(detailRes.err)
if (detailRes.data == null) return Resource.Error(GlassfyErrorCode.NotFoundOnGlassfy.toError())

Resource.Success(it.apply {
product = detailRes.data.first()
})
}
return matchSkuWithStoreDetails(skuRes.data, detailRes.data)?.let {
skuRes.data.product = it
Resource.Success(skuRes.data)
} ?: Resource.Error(GlassfyErrorCode.NotFoundOnGlassfy.toError())
}

private suspend fun _connectCustomSubscriber(customId: String?): Resource<Unit> {
Expand Down Expand Up @@ -418,22 +439,21 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {
if (pRes.err != null) return Resource.Error(pRes.err)
if (pRes.data == null || pRes.data.skus.isEmpty()) return Resource.Error(GlassfyErrorCode.NotFoundOnGlassfy.toError())

val dRes =
pRes.data.skus.map { s -> s.productId }.toSet().let { billingService.skuDetails(it) }
val dRes = billingService.skusDetails(SkuDetailsQuery.fromSkus(pRes.data.skus))
if (dRes.err != null) return Resource.Error(dRes.err)
if (dRes.data == null) return Resource.Error(GlassfyErrorCode.NotFoundOnStore.toError())

pRes.data.skus =
pRes.data.skus.filter { sku -> dRes.data.map { it.sku }.contains(sku.productId) }
pRes.data.skus.forEach { s ->
dRes.data.find { detail -> detail.sku == s.productId }
?.let { detail -> s.product = detail }
pRes.data.skus = pRes.data.skus.mapNotNull { s ->
matchSkuWithStoreDetails(s, dRes.data)?.let {
s.apply {
product = it
}
}
}

return Resource.Success(pRes.data)
}

/// LifecycleEventObserver
/// LifecycleEventObserver

override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Expand All @@ -450,7 +470,7 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {
private suspend fun onStartProcessState() = withSdkInitialized()?.also { repository.lastSeen() }


/// Utils
/// Utils

private fun makeApiService(
cacheManager: ICacheManager, deviceManager: IDeviceManager, apiKey: String
Expand All @@ -466,11 +486,14 @@ internal class GManager : LifecycleEventObserver, IBillingPurchaseDelegate {
val r = c.request().newBuilder().header("Authorization", "Bearer $apiKey").url(url)
.build()
c.proceed(r)
}.build()
}
// .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.build()

val moshi =
Moshi.Builder().add(EntitlementAdapter()).add(StoreAdapter()).add(StoreInfoAdapter())
.add(EventTypeAdapter()).add(UserPropertiesAdapter()).add(PaywallTypeAdapter())
Moshi.Builder().add(EntitlementAdapter()).add(ProductTypeAdapter()).add(StoreAdapter())
.add(StoreInfoAdapter()).add(EventTypeAdapter()).add(UserPropertiesAdapter())
.add(PaywallTypeAdapter())
// .addLast(KotlinJsonAdapterFactory()) if not using Codegen, use Reflection (2.5 MiB .jar file)
.build()

Expand Down
Loading

0 comments on commit 7457834

Please sign in to comment.