Skip to content

Commit eecea4d

Browse files
committed
version 3.5.0
1 parent bc2cc3f commit eecea4d

29 files changed

+1018
-229
lines changed

adapty/src/main/java/com/adapty/errors/AdaptyErrorCode.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public enum class AdaptyErrorCode(@get:JvmSynthetic internal val value: Int) {
2626
REQUEST_FAILED(2005),
2727
DECODING_FAILED(2006),
2828
ANALYTICS_DISABLED(3000),
29-
WRONG_PARAMETER(3001);
29+
WRONG_PARAMETER(3001),
30+
PROFILE_WAS_CHANGED(3006);
3031

3132
public companion object {
3233
@Deprecated(

adapty/src/main/java/com/adapty/internal/data/cache/CacheEntity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ internal class CacheEntity<T>(
88
@SerializedName("value")
99
val value: T,
1010
@SerializedName("version")
11-
val version: Int,
11+
val version: Int = 1,
1212
@SerializedName("cached_at")
1313
val cachedAt: Long = System.currentTimeMillis(),
1414
) {

adapty/src/main/java/com/adapty/internal/data/cache/CacheKeys.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package com.adapty.internal.data.cache
1111
@JvmSynthetic internal const val IAM_SESSION_TOKEN = "IAM_SESSION_TOKEN"
1212
@JvmSynthetic internal const val PURCHASER_INFO = "PURCHASER_INFO"
1313
@JvmSynthetic internal const val PROFILE = "PROFILE"
14+
@JvmSynthetic internal const val CROSS_PLACEMENT_INFO = "CROSS_PLACEMENT_INFO"
1415
@JvmSynthetic internal const val CONTAINERS = "CONTAINERS"
1516
@JvmSynthetic internal const val FALLBACK_PAYWALLS = "FALLBACK_PAYWALLS"
1617
@JvmSynthetic internal const val PRODUCTS = "PRODUCTS"
@@ -23,6 +24,7 @@ package com.adapty.internal.data.cache
2324
@JvmSynthetic internal const val PURCHASES_HAVE_BEEN_SYNCED = "PURCHASES_HAVE_BEEN_SYNCED"
2425
@JvmSynthetic internal const val SESSION_ID = "SESSION_ID"
2526
@JvmSynthetic internal const val APP_OPENED_TIME = "APP_OPENED_TIME"
27+
@JvmSynthetic internal const val CROSSPLACEMENT_INFO_REQUESTED_TIME = "CROSSPLACEMENT_INFO_REQUESTED_TIME"
2628
@JvmSynthetic internal const val APP_KEY = "APP_KEY"
2729
@JvmSynthetic internal const val PAYWALL_RESPONSE_START_PART = "get_paywall_"
2830
@JvmSynthetic internal const val PAYWALL_RESPONSE_END_PART = "_response"

adapty/src/main/java/com/adapty/internal/data/cache/CacheRepository.kt

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
@file:OptIn(InternalAdaptyApi::class)
2+
13
package com.adapty.internal.data.cache
24

35
import androidx.annotation.RestrictTo
46
import com.adapty.internal.data.models.*
57
import com.adapty.internal.utils.FallbackPaywallRetriever
8+
import com.adapty.internal.utils.InternalAdaptyApi
9+
import com.adapty.internal.utils.ProfileStateChange
610
import com.adapty.internal.utils.execute
711
import com.adapty.internal.utils.extractLanguageCode
812
import com.adapty.internal.utils.generateUuid
913
import com.adapty.internal.utils.getLanguageCode
1014
import com.adapty.internal.utils.orDefault
15+
import com.adapty.internal.utils.unlockQuietly
1116
import com.adapty.utils.FileLocation
1217
import kotlinx.coroutines.flow.MutableSharedFlow
1318
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -19,6 +24,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock
1924
internal class CacheRepository(
2025
private val preferenceManager: PreferenceManager,
2126
private val fallbackPaywallRetriever: FallbackPaywallRetriever,
27+
private val crossPlacementInfoLock: ReentrantReadWriteLock,
2228
) {
2329

2430
private val currentProfile = MutableSharedFlow<ProfileDto>()
@@ -29,15 +35,14 @@ internal class CacheRepository(
2935
suspend fun updateDataOnCreateProfile(
3036
profile: ProfileDto,
3137
installationMeta: InstallationMeta,
32-
): Boolean {
33-
if (profile.timestamp.orDefault() < getProfile()?.timestamp.orDefault())
34-
return false
35-
var profileIdHasChanged = false
36-
(profile.profileId ?: (cache[UNSYNCED_PROFILE_ID] as? String))?.let { profileId ->
37-
profileIdHasChanged = profileId != preferenceManager.getString(PROFILE_ID)
38-
39-
if (profileIdHasChanged) onNewProfileIdReceived(profileId)
40-
}
38+
profileStateChange: ProfileStateChange,
39+
) {
40+
if (profileStateChange == ProfileStateChange.OUTDATED)
41+
return
42+
val profileIdHasChanged =
43+
profileStateChange in listOf(ProfileStateChange.NEW, ProfileStateChange.IDENTIFIED_TO_ANOTHER)
44+
if (profileIdHasChanged)
45+
onNewProfileIdReceived(profile.profileId)
4146
if (profileIdHasChanged || (getCustomerUserId() != profile.customerUserId)) {
4247
clearSyncedPurchases()
4348
}
@@ -49,7 +54,6 @@ internal class CacheRepository(
4954
if (!currentUnsyncedCUIdDiffers)
5055
cache.remove(UNSYNCED_CUSTOMER_USER_ID)
5156
saveLastSentInstallationMeta(installationMeta)
52-
return profileIdHasChanged
5357
}
5458

5559
@JvmSynthetic
@@ -218,6 +222,26 @@ internal class CacheRepository(
218222
preferenceManager.saveLong(APP_OPENED_TIME, timeMillis)
219223
}
220224

225+
@JvmSynthetic
226+
fun getLastRequestedCrossPlacementInfoTime() =
227+
cache.safeGetOrPut(
228+
CROSSPLACEMENT_INFO_REQUESTED_TIME,
229+
{ preferenceManager.getLong(CROSSPLACEMENT_INFO_REQUESTED_TIME, 0L) }) as? Long ?: 0L
230+
231+
@JvmSynthetic
232+
fun saveLastRequestedCrossPlacementInfoTime(timeMillis: Long) {
233+
cache[CROSSPLACEMENT_INFO_REQUESTED_TIME] = timeMillis
234+
preferenceManager.saveLong(CROSSPLACEMENT_INFO_REQUESTED_TIME, timeMillis)
235+
}
236+
237+
@JvmSynthetic
238+
fun clearLastRequestedCrossPlacementInfoTime() {
239+
clearData(
240+
containsKeys = setOf(CROSSPLACEMENT_INFO_REQUESTED_TIME),
241+
startsWithKeys = setOf(),
242+
)
243+
}
244+
221245
@JvmSynthetic
222246
fun getProfile() =
223247
getData(PROFILE, ProfileDto::class.java)
@@ -284,6 +308,45 @@ internal class CacheRepository(
284308
cache[FALLBACK_PAYWALLS] = fallbackPaywallRetriever.getMetaInfo(source)
285309
}
286310

311+
@JvmSynthetic
312+
fun getCrossPlacementInfo() =
313+
try {
314+
crossPlacementInfoLock.readLock().lock()
315+
getCrossPlacementInfoInternal()
316+
} finally {
317+
crossPlacementInfoLock.readLock().unlockQuietly()
318+
}
319+
320+
private fun getCrossPlacementInfoInternal() =
321+
getData<CacheEntity<CrossPlacementInfo>>(CROSS_PLACEMENT_INFO)?.value
322+
323+
fun saveCrossPlacementInfo(crossPlacementInfo: CrossPlacementInfo) {
324+
try {
325+
crossPlacementInfoLock.writeLock().lock()
326+
val oldVersion = getCrossPlacementInfoInternal()?.version ?: -1
327+
if (crossPlacementInfo.version > oldVersion)
328+
saveData(CROSS_PLACEMENT_INFO, CacheEntity(crossPlacementInfo))
329+
} finally {
330+
crossPlacementInfoLock.writeLock().unlockQuietly()
331+
}
332+
}
333+
334+
fun saveCrossPlacementInfoFromPaywall(crossPlacementInfo: CrossPlacementInfo) {
335+
try {
336+
crossPlacementInfoLock.writeLock().lock()
337+
saveData(
338+
CROSS_PLACEMENT_INFO,
339+
CacheEntity(
340+
getCrossPlacementInfoInternal()
341+
?.copy(placementWithVariationMap = crossPlacementInfo.placementWithVariationMap)
342+
?: crossPlacementInfo
343+
)
344+
)
345+
} finally {
346+
crossPlacementInfoLock.writeLock().unlockQuietly()
347+
}
348+
}
349+
287350
@JvmSynthetic
288351
fun clearOnLogout() {
289352
clearData(
@@ -294,10 +357,12 @@ internal class CacheRepository(
294357
SYNCED_PURCHASES,
295358
PURCHASES_HAVE_BEEN_SYNCED,
296359
APP_OPENED_TIME,
360+
CROSSPLACEMENT_INFO_REQUESTED_TIME,
297361
PRODUCT_RESPONSE,
298362
PRODUCT_RESPONSE_HASH,
299363
PROFILE_RESPONSE,
300364
PROFILE_RESPONSE_HASH,
365+
CROSS_PLACEMENT_INFO,
301366
),
302367
startsWithKeys = setOf(PAYWALL_RESPONSE_START_PART),
303368
)
@@ -324,12 +389,14 @@ internal class CacheRepository(
324389
SYNCED_PURCHASES,
325390
PURCHASES_HAVE_BEEN_SYNCED,
326391
APP_OPENED_TIME,
392+
CROSSPLACEMENT_INFO_REQUESTED_TIME,
327393
PRODUCT_RESPONSE,
328394
PRODUCT_RESPONSE_HASH,
329395
PRODUCT_IDS_RESPONSE,
330396
PRODUCT_IDS_RESPONSE_HASH,
331397
PROFILE_RESPONSE,
332398
PROFILE_RESPONSE_HASH,
399+
CROSS_PLACEMENT_INFO,
333400
ANALYTICS_DATA,
334401
YET_UNPROCESSED_VALIDATE_PRODUCT_INFO,
335402
EXTERNAL_ANALYTICS_ENABLED,

adapty/src/main/java/com/adapty/internal/data/cloud/CloudRepository.kt

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,23 @@ internal class CloudRepository(
4444
}
4545
}
4646

47-
fun getPaywallVariations(id: String, locale: String, segmentId: String): Variations {
47+
fun getPaywallVariations(id: String, locale: String, segmentId: String): Pair<Variations, Request.CurrentDataWhenSent?> {
48+
val request = requestFactory.getPaywallVariationsRequest(id, locale, segmentId)
4849
val response = httpClient.newCall<Variations>(
49-
requestFactory.getPaywallVariationsRequest(id, locale, segmentId),
50+
request,
5051
Variations::class.java,
5152
)
53+
when (response) {
54+
is Response.Success -> return response.body to request.currentDataWhenSent
55+
is Response.Error -> throw response.error
56+
}
57+
}
58+
59+
fun getPaywallByVariationId(id: String, locale: String, segmentId: String, variationId: String): PaywallDto {
60+
val response = httpClient.newCall<PaywallDto>(
61+
requestFactory.getPaywallByVariationIdRequest(id, locale, segmentId, variationId),
62+
PaywallDto::class.java,
63+
)
5264
when (response) {
5365
is Response.Success -> return response.body
5466
is Response.Error -> throw response.error
@@ -74,6 +86,25 @@ internal class CloudRepository(
7486
}
7587
}
7688

89+
@JvmSynthetic
90+
fun getPaywallByVariationIdFallback(id: String, locale: String, variationId: String): PaywallDto {
91+
val response = httpClient.newCall<PaywallDto>(
92+
requestFactory.getPaywallByVariationIdFallbackRequest(id, locale, variationId),
93+
PaywallDto::class.java,
94+
)
95+
when (response) {
96+
is Response.Success -> return response.body
97+
is Response.Error -> {
98+
val error = response.error
99+
when {
100+
error.adaptyErrorCode == AdaptyErrorCode.BAD_REQUEST && locale != DEFAULT_PAYWALL_LOCALE ->
101+
return getPaywallByVariationIdFallback(id, DEFAULT_PAYWALL_LOCALE, variationId)
102+
else -> throw response.error
103+
}
104+
}
105+
}
106+
}
107+
77108
@JvmSynthetic
78109
fun getPaywallVariationsUntargeted(id: String, locale: String): Variations {
79110
val response = httpClient.newCall<Variations>(
@@ -230,6 +261,19 @@ internal class CloudRepository(
230261
processEmptyResponse(response)
231262
}
232263

264+
@JvmSynthetic
265+
fun getCrossPlacementInfo(replacementProfileId: String?): CrossPlacementInfo {
266+
val response = httpClient.newCall<CrossPlacementInfo>(
267+
requestFactory.getCrossPlacementInfoRequest(replacementProfileId),
268+
CrossPlacementInfo::class.java
269+
)
270+
271+
when (response) {
272+
is Response.Success -> return response.body
273+
is Response.Error -> throw response.error
274+
}
275+
}
276+
233277
@JvmSynthetic
234278
fun reportTransactionWithVariation(
235279
transactionId: String,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.adapty.internal.data.cloud
2+
3+
import com.adapty.internal.utils.SinglePaywallExtractHelper
4+
import com.google.gson.JsonArray
5+
import com.google.gson.JsonElement
6+
import com.google.gson.JsonObject
7+
import com.google.gson.JsonPrimitive
8+
9+
internal class FallbackVariationsExtractor(
10+
private val singlePaywallExtractHelper: SinglePaywallExtractHelper,
11+
): ResponseDataExtractor {
12+
13+
override fun extract(jsonElement: JsonElement): JsonElement? {
14+
val jsonObject = jsonElement.asJsonObject
15+
16+
val meta = jsonObject.remove(metaKey) as? JsonObject
17+
val snapshotAt = (meta?.get(responseCreatedAtKey) as? JsonPrimitive) ?: JsonPrimitive(0)
18+
19+
val variations = JsonArray()
20+
21+
jsonObject.getAsJsonObject(dataKey).entrySet()
22+
.first { (key, value) ->
23+
val desiredArray = (value as? JsonArray)?.isEmpty == false
24+
desiredArray.also {
25+
if (desiredArray) jsonObject.addProperty(placementIdKey, key)
26+
}
27+
}
28+
.value.asJsonArray
29+
.forEach { element ->
30+
((element as? JsonObject)?.get(attributesKey) as? JsonObject)
31+
?.let { paywall ->
32+
singlePaywallExtractHelper.addSnapshotAtIfMissing(paywall, snapshotAt)
33+
variations.add(paywall)
34+
}
35+
}
36+
37+
jsonObject.add(dataKey, variations)
38+
return jsonObject
39+
}
40+
41+
private companion object {
42+
const val dataKey = "data"
43+
const val attributesKey = "attributes"
44+
const val metaKey = "meta"
45+
const val placementIdKey = "placement_id"
46+
const val responseCreatedAtKey = "response_created_at"
47+
}
48+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.adapty.internal.data.cloud
2+
3+
import com.adapty.errors.AdaptyError
4+
import com.adapty.errors.AdaptyErrorCode
5+
import com.google.gson.JsonElement
6+
import com.google.gson.JsonObject
7+
8+
internal class ProfileExtractor: ResponseDataExtractor {
9+
10+
override fun extract(jsonElement: JsonElement): JsonElement? {
11+
val jsonObject = jsonElement.asJsonObject
12+
13+
if (!jsonObject.has(dataKey))
14+
return extractInternal(jsonObject)
15+
16+
val data = jsonObject.getAsJsonObject(dataKey).getAsJsonObject(attributesKey)
17+
18+
return extractInternal(data)
19+
}
20+
21+
private fun extractInternal(jsonObject: JsonObject): JsonElement? {
22+
jsonObject.requires("profile_id") { "profileId in Profile should not be null" }
23+
jsonObject.requires("segment_hash") { "segmentHash in Profile should not be null" }
24+
25+
return jsonObject
26+
}
27+
28+
private inline fun JsonObject.requires(key: String, errorMessage: () -> String) {
29+
if (!has(key))
30+
throw AdaptyError(
31+
message = errorMessage(),
32+
adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED,
33+
)
34+
}
35+
36+
private companion object {
37+
const val dataKey = "data"
38+
const val attributesKey = "attributes"
39+
}
40+
}

0 commit comments

Comments
 (0)