Skip to content

Commit 6e9aef5

Browse files
Merge branch 'development' into release/1.204
2 parents 6beee82 + 55fd809 commit 6e9aef5

18 files changed

Lines changed: 287 additions & 94 deletions

File tree

app/src/debug/java/org/stepik/android/view/debug/ui/adapter/delegate/InAppPurchaseAdapterDelegate.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,6 @@ class InAppPurchaseAdapterDelegate(
3535

3636
inAppPurchaseCourse.isVisible = data.developerPayload.isNotEmpty()
3737
inAppPurchaseUser.isVisible = data.developerPayload.isNotEmpty()
38-
if (data.developerPayload.isNotEmpty()) {
39-
data.developerPayload.toObject<CoursePurchasePayload>().let {
40-
inAppPurchaseCourse.text = context.getString(R.string.debug_purchase_course, it.courseId)
41-
inAppPurchaseUser.text = context.getString(R.string.debug_purchase_profile, it.profileId)
42-
}
43-
}
4438
}
4539
}
4640
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.stepic.droid.storage.migration
2+
3+
import androidx.room.migration.Migration
4+
import androidx.sqlite.db.SupportSQLiteDatabase
5+
6+
object MigrationFrom77To78 : Migration(77, 78) {
7+
override fun migrate(db: SupportSQLiteDatabase) {
8+
db.execSQL("CREATE TABLE IF NOT EXISTS `BillingPurchasePayload` (`orderId` TEXT NOT NULL, `courseId` INTEGER NOT NULL, `profileId` INTEGER NOT NULL, `obfuscatedAccountId` TEXT NOT NULL, `obfuscatedProfileId` TEXT NOT NULL, `promoCode` TEXT, PRIMARY KEY(`orderId`))")
9+
}
10+
}

app/src/main/java/org/stepic/droid/storage/migration/Migrations.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ object Migrations {
208208
MigrationFrom73To74,
209209
MigrationFrom74To75,
210210
MigrationFrom75To76,
211-
MigrationFrom76To77
211+
MigrationFrom76To77,
212+
MigrationFrom77To78
212213
)
213214
}

app/src/main/java/org/stepik/android/cache/base/database/AppDatabase.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import org.stepik.android.domain.catalog.model.CatalogBlock
1818
import org.stepik.android.domain.review_instruction.model.ReviewInstruction
1919
import org.stepik.android.domain.review_session.model.ReviewSession
2020
import org.stepik.android.cache.code_preference.model.CodePreference
21+
import org.stepik.android.cache.course_purchase.dao.BillingPurchasePayloadDao
2122
import org.stepik.android.cache.course_recommendations.dao.CourseRecommendationsDao
2223
import org.stepik.android.cache.exam_session.dao.ExamSessionDao
2324
import org.stepik.android.cache.mobile_tiers.dao.LightSkuDao
2425
import org.stepik.android.cache.mobile_tiers.dao.MobileTiersDao
2526
import org.stepik.android.cache.proctor_session.dao.ProctorSessionDao
2627
import org.stepik.android.cache.rubric.dao.RubricDao
2728
import org.stepik.android.cache.wishlist.dao.WishlistDao
29+
import org.stepik.android.domain.course_purchase.model.BillingPurchasePayload
2830
import org.stepik.android.domain.course_recommendations.model.CourseRecommendation
2931
import org.stepik.android.domain.exam_session.model.ExamSession
3032
import org.stepik.android.domain.mobile_tiers.model.LightSku
@@ -48,7 +50,8 @@ import org.stepik.android.domain.wishlist.model.WishlistEntry
4850
ProctorSession::class,
4951
MobileTier::class,
5052
LightSku::class,
51-
WishlistEntry::class
53+
WishlistEntry::class,
54+
BillingPurchasePayload::class
5255
],
5356
version = AppDatabase.VERSION,
5457
exportSchema = false
@@ -62,7 +65,7 @@ import org.stepik.android.domain.wishlist.model.WishlistEntry
6265
)
6366
abstract class AppDatabase : RoomDatabase() {
6467
companion object {
65-
const val VERSION = 77
68+
const val VERSION = 78
6669
const val NAME = "stepic_database.db"
6770
}
6871

@@ -79,4 +82,5 @@ abstract class AppDatabase : RoomDatabase() {
7982
abstract fun mobileTiersDao(): MobileTiersDao
8083
abstract fun lightSkuDao(): LightSkuDao
8184
abstract fun wishlistDao(): WishlistDao
85+
abstract fun billingPurchasePayloadDao(): BillingPurchasePayloadDao
8286
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.stepik.android.cache.course_purchase
2+
3+
import io.reactivex.Completable
4+
import io.reactivex.Single
5+
import org.stepik.android.cache.course_purchase.dao.BillingPurchasePayloadDao
6+
import org.stepik.android.data.course_purchase.source.BillingPurchasePayloadCacheDataSource
7+
import org.stepik.android.domain.course_purchase.model.BillingPurchasePayload
8+
import javax.inject.Inject
9+
10+
class BillingPurchasePayloadCacheDataSourceImpl
11+
@Inject
12+
constructor(
13+
private val billingPurchasePayloadDao: BillingPurchasePayloadDao
14+
) : BillingPurchasePayloadCacheDataSource {
15+
override fun getPurchaseOrderPayload(orderId: String): Single<BillingPurchasePayload> =
16+
billingPurchasePayloadDao.getBillingPurchasePayload(orderId)
17+
18+
override fun savePurchaseOrderPayload(billingPurchasePayload: BillingPurchasePayload): Completable =
19+
billingPurchasePayloadDao.saveBillingPurchasePayload(billingPurchasePayload)
20+
21+
override fun deletePurchaseOrderPayload(orderId: String): Completable =
22+
billingPurchasePayloadDao.deleteBillingPurchasePayload(orderId)
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.stepik.android.cache.course_purchase.dao
2+
3+
import androidx.room.Dao
4+
import androidx.room.Query
5+
import androidx.room.Insert
6+
import androidx.room.OnConflictStrategy
7+
import io.reactivex.Completable
8+
import io.reactivex.Single
9+
import org.stepik.android.domain.course_purchase.model.BillingPurchasePayload
10+
11+
@Dao
12+
interface BillingPurchasePayloadDao {
13+
@Query("SELECT * FROM BillingPurchasePayload WHERE orderId = :orderId")
14+
fun getBillingPurchasePayload(orderId: String): Single<BillingPurchasePayload>
15+
16+
@Insert(onConflict = OnConflictStrategy.REPLACE)
17+
fun saveBillingPurchasePayload(billingPurchasePayload: BillingPurchasePayload): Completable
18+
19+
@Query("DELETE FROM BillingPurchasePayload WHERE orderId = :orderId")
20+
fun deleteBillingPurchasePayload(orderId: String): Completable
21+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.stepik.android.data.course_purchase.repository
2+
3+
import io.reactivex.Completable
4+
import io.reactivex.Single
5+
import org.stepik.android.data.course_purchase.source.BillingPurchasePayloadCacheDataSource
6+
import org.stepik.android.domain.course_purchase.model.BillingPurchasePayload
7+
import org.stepik.android.domain.course_purchase.repository.BillingPurchasePayloadRepository
8+
import javax.inject.Inject
9+
10+
class BillingPurchasePayloadRepositoryImpl
11+
@Inject
12+
constructor(
13+
private val billingPurchasePayloadCacheDataSource: BillingPurchasePayloadCacheDataSource
14+
) : BillingPurchasePayloadRepository {
15+
override fun getBillingPurchasePayload(orderId: String): Single<BillingPurchasePayload> =
16+
billingPurchasePayloadCacheDataSource.getPurchaseOrderPayload(orderId)
17+
18+
override fun saveBillingPurchasePayload(billingPurchasePayload: BillingPurchasePayload): Completable =
19+
billingPurchasePayloadCacheDataSource.savePurchaseOrderPayload(billingPurchasePayload)
20+
21+
override fun deleteBillingPurchasePayload(orderId: String): Completable =
22+
billingPurchasePayloadCacheDataSource.deletePurchaseOrderPayload(orderId)
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.stepik.android.data.course_purchase.source
2+
3+
import io.reactivex.Completable
4+
import io.reactivex.Single
5+
import org.stepik.android.domain.course_purchase.model.BillingPurchasePayload
6+
7+
interface BillingPurchasePayloadCacheDataSource {
8+
fun getPurchaseOrderPayload(orderId: String): Single<BillingPurchasePayload>
9+
fun savePurchaseOrderPayload(billingPurchasePayload: BillingPurchasePayload): Completable
10+
fun deletePurchaseOrderPayload(orderId: String): Completable
11+
}

app/src/main/java/org/stepik/android/domain/course_purchase/interactor/CoursePurchaseInteractor.kt

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import com.android.billingclient.api.BillingClient
44
import com.android.billingclient.api.Purchase
55
import com.android.billingclient.api.SkuDetails
66
import io.reactivex.Completable
7-
import io.reactivex.Maybe
87
import io.reactivex.Single
9-
import io.reactivex.rxkotlin.Maybes.zip
8+
import io.reactivex.rxkotlin.Singles
109
import io.reactivex.subjects.PublishSubject
1110
import org.stepik.android.domain.base.DataSourceType
1211
import org.stepik.android.domain.billing.exception.NoPurchasesToRestoreException
@@ -18,7 +17,10 @@ import org.stepik.android.domain.course_payments.exception.CoursePurchaseVerific
1817
import org.stepik.android.domain.course_payments.model.CoursePayment
1918
import org.stepik.android.domain.course_payments.model.PromoCodeSku
2019
import org.stepik.android.domain.course_payments.repository.CoursePaymentsRepository
20+
import org.stepik.android.domain.course_purchase.model.BillingPurchasePayload
2121
import org.stepik.android.domain.course_purchase.model.CoursePurchaseObfuscatedParams
22+
import org.stepik.android.domain.course_purchase.model.PurchaseFlowData
23+
import org.stepik.android.domain.course_purchase.repository.BillingPurchasePayloadRepository
2224
import org.stepik.android.domain.lesson.repository.LessonRepository
2325
import org.stepik.android.domain.mobile_tiers.repository.LightSkuRepository
2426
import org.stepik.android.domain.mobile_tiers.repository.MobileTiersRepository
@@ -27,7 +29,7 @@ import org.stepik.android.domain.user_courses.interactor.UserCoursesInteractor
2729
import org.stepik.android.model.Course
2830
import org.stepik.android.remote.mobile_tiers.model.MobileTierCalculation
2931
import org.stepik.android.view.injection.course.EnrollmentCourseUpdates
30-
import ru.nobird.android.domain.rx.maybeFirst
32+
import ru.nobird.android.domain.rx.toMaybe
3133
import javax.inject.Inject
3234

3335
class CoursePurchaseInteractor
@@ -42,6 +44,7 @@ constructor(
4244
private val courseRepository: CourseRepository,
4345
private val lessonRepository: LessonRepository,
4446
private val profileRepository: ProfileRepository,
47+
private val billingPurchasePayloadRepository: BillingPurchasePayloadRepository,
4548

4649
@EnrollmentCourseUpdates
4750
private val enrollmentSubject: PublishSubject<Course>
@@ -60,7 +63,7 @@ constructor(
6063
}
6164
}
6265

63-
fun fetchPurchaseFlowData(courseId: Long, skuId: String): Single<Pair<CoursePurchaseObfuscatedParams, SkuDetails>> =
66+
fun fetchPurchaseFlowData(courseId: Long, skuId: String): Single<PurchaseFlowData> =
6467
coursePaymentsRepository
6568
.getCoursePaymentsByCourseId(courseId, CoursePayment.Status.SUCCESS, sourceType = DataSourceType.REMOTE)
6669
.flatMap { payments ->
@@ -71,7 +74,48 @@ constructor(
7174
}
7275
}
7376

74-
fun completePurchase(courseId: Long, sku: SkuDetails, purchase: Purchase, promoCodeName: String? = null): Completable =
77+
fun consumePurchase(
78+
courseId: Long,
79+
profileId: Long,
80+
sku: SkuDetails,
81+
purchase: Purchase,
82+
promoCodeName: String? = null
83+
): Completable =
84+
saveBillingPurchasePayload(courseId, profileId, purchase, promoCodeName)
85+
.andThen(completePurchase(courseId, sku, purchase, promoCodeName))
86+
87+
fun restorePurchase(courseId: Long): Completable =
88+
Singles.zip(
89+
getCurrentProfileId(),
90+
billingRepository.getAllPurchases(BillingClient.SkuType.INAPP)
91+
).flatMapMaybe { (profileId, purchases) ->
92+
val obfuscatedParams = createCoursePurchaseObfuscatedParams(profileId, courseId)
93+
val purchase =
94+
purchases.find {
95+
it.accountIdentifiers?.obfuscatedAccountId == obfuscatedParams.obfuscatedAccountId &&
96+
it.accountIdentifiers?.obfuscatedProfileId == obfuscatedParams.obfuscatedProfileId
97+
}
98+
purchase.toMaybe()
99+
}
100+
.switchIfEmpty(Single.error(NoPurchasesToRestoreException()))
101+
.flatMapCompletable { purchase ->
102+
completePurchaseRestore(courseId, purchase)
103+
}
104+
105+
private fun saveBillingPurchasePayload(courseId: Long, profileId: Long, purchase: Purchase, promoCodeName: String?): Completable =
106+
billingPurchasePayloadRepository
107+
.saveBillingPurchasePayload(
108+
BillingPurchasePayload(
109+
orderId = purchase.orderId,
110+
courseId = courseId,
111+
profileId = profileId,
112+
obfuscatedAccountId = purchase.accountIdentifiers?.obfuscatedAccountId.orEmpty(),
113+
obfuscatedProfileId = purchase.accountIdentifiers?.obfuscatedProfileId.orEmpty(),
114+
promoCode = promoCodeName
115+
)
116+
)
117+
118+
private fun completePurchase(courseId: Long, sku: SkuDetails, purchase: Purchase, promoCodeName: String? = null): Completable =
75119
coursePaymentsRepository
76120
.createCoursePayment(courseId, sku, purchase, promoCodeName)
77121
.flatMapCompletable { payment ->
@@ -82,35 +126,38 @@ constructor(
82126
}
83127
}
84128
.andThen(billingRepository.consumePurchase(purchase))
129+
.andThen(billingPurchasePayloadRepository.deleteBillingPurchasePayload(purchase.orderId))
85130
.andThen(updateCourseAfterEnrollment(courseId))
86131

87-
fun fetchSkuDetailsAndPurchase(skuId: String): Maybe<Pair<SkuDetails, Purchase>> =
88-
zip(
89-
billingRepository
90-
.getInventory(BillingClient.SkuType.INAPP, skuId),
91-
billingRepository
92-
.getAllPurchases(BillingClient.SkuType.INAPP, listOf(skuId))
93-
.maybeFirst()
94-
)
95-
96-
fun restorePurchase(courseId: Long, sku: SkuDetails, purchase: Purchase): Completable =
97-
getCurrentProfileId()
98-
.filter { profileId ->
99-
val obfuscatedParams = createCoursePurchaseObfuscatedParams(profileId, courseId)
100-
purchase.accountIdentifiers?.obfuscatedAccountId == obfuscatedParams.obfuscatedAccountId && purchase.accountIdentifiers?.obfuscatedProfileId == obfuscatedParams.obfuscatedProfileId
101-
}
102-
.switchIfEmpty(Single.error(NoPurchasesToRestoreException()))
103-
.flatMapCompletable {
104-
completePurchase(courseId, sku, purchase)
105-
}
132+
private fun completePurchaseRestore(courseId: Long, purchase: Purchase): Completable =
133+
Singles.zip(
134+
billingPurchasePayloadRepository.getBillingPurchasePayload(purchase.orderId).onErrorReturnItem(BillingPurchasePayload.EMPTY),
135+
billingRepository.getInventory(BillingClient.SkuType.INAPP, purchase.skus.first()).toSingle()
136+
).flatMapCompletable { (billingPurchasePayload, skuDetails) ->
137+
val promoCode =
138+
if (billingPurchasePayload.obfuscatedAccountId == purchase.accountIdentifiers?.obfuscatedAccountId &&
139+
billingPurchasePayload.obfuscatedProfileId == purchase.accountIdentifiers?.obfuscatedProfileId
140+
) {
141+
billingPurchasePayload.promoCode
142+
} else {
143+
null
144+
}
145+
completePurchase(courseId, skuDetails, purchase, promoCode)
146+
}
106147

107-
private fun getSkuDetails(courseId: Long, skuId: String): Single<Pair<CoursePurchaseObfuscatedParams, SkuDetails>> =
148+
private fun getSkuDetails(courseId: Long, skuId: String): Single<PurchaseFlowData> =
108149
getCurrentProfileId()
109150
.flatMap { profileId ->
110151
billingRepository
111152
.getInventory(BillingClient.SkuType.INAPP, skuId)
112153
.toSingle()
113-
.map { skuDetails -> createCoursePurchaseObfuscatedParams(profileId, courseId) to skuDetails }
154+
.map { skuDetails ->
155+
PurchaseFlowData(
156+
CoursePurchasePayload(profileId, courseId),
157+
createCoursePurchaseObfuscatedParams(profileId, courseId),
158+
skuDetails
159+
)
160+
}
114161
}
115162

116163
private fun updateCourseAfterEnrollment(courseId: Long): Completable =
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.stepik.android.domain.course_purchase.model
2+
3+
import androidx.room.Entity
4+
import androidx.room.PrimaryKey
5+
6+
@Entity
7+
data class BillingPurchasePayload(
8+
@PrimaryKey
9+
val orderId: String,
10+
val courseId: Long,
11+
val profileId: Long,
12+
val obfuscatedAccountId: String,
13+
val obfuscatedProfileId: String,
14+
val promoCode: String?
15+
) {
16+
companion object {
17+
val EMPTY = BillingPurchasePayload("", 0L, 0L, "", "", null)
18+
}
19+
}

0 commit comments

Comments
 (0)