From c9ed186213c46c68f8b91e278cdd35e5ffa2f2ae Mon Sep 17 00:00:00 2001 From: Eugene Popovich Date: Thu, 15 Apr 2021 15:15:08 +0300 Subject: [PATCH] Add license flavor dimension and split app into 2 versions "Proprietary" and "NoProprietary" --- app/build.gradle.kts | 44 ++++- app/lint.xml | 6 +- .../res/values/titles.xml | 7 + .../res/values/titles.xml | 6 +- .../res/values/titles.xml | 7 + .../res/values/titles.xml | 6 +- .../domain/entities}/ResultWrapper.kt | 55 ++---- .../payment/PurchaseAdapter.kt | 4 +- .../payment/PurchaseItem.kt | 4 +- .../payment/PurchaseItemViewState.kt | 4 +- .../payment/PurchaseViewEffect.kt | 4 +- .../payment/PurchaseViewModel.kt | 46 +++-- .../payment/PurchaseViewState.kt | 14 +- .../PurchaseViewStatePartialChanges.kt | 9 +- .../billing/data/source/BillingErrorCodes.kt | 46 ----- .../domain/entities/BillingErrorCodes.kt | 13 ++ .../billing/domain/entities/ConsumeResult.kt | 5 + .../billing/domain/entities/Purchase.kt | 8 + .../billing/domain/entities/PurchaseState.kt | 7 + .../billing/domain/entities/SkuDetails.kt | 8 + .../billing/domain/entities/SkuType.kt | 6 + .../domain/repository/PurchaseRepository.kt | 49 ++++++ .../payment/data/model/Purchase.kt | 6 - .../payment/di/PurchaseDataModule.kt | 23 +-- .../payment/model/Purchase.kt | 6 + .../ReminderNotificationListenerService.kt | 3 +- .../settings/SettingsViewModel.kt | 42 +++-- .../util/coroutines/CoroutinesExtensions.kt | 6 +- .../billing/data/PurchaseRepositoryImpl.kt | 52 ++++++ .../payment/di/PurchaseRepositoryModule.kt | 20 +++ .../res/values/titles.xml | 7 + .../res/values/titles.xml | 6 +- .../billing/data/PurchaseRepositoryImpl.kt} | 163 ++++++++++++------ .../billing/data/mappers/PurchaseMappers.kt | 19 ++ .../billing/data/mappers/SkuDetailsMappers.kt | 20 +++ .../billing/data/mappers/SkuTypeMappers.kt | 12 ++ .../remote/BillingOperationException.kt | 28 +-- .../data/source/remote/CoroutinesBilling.kt | 0 .../billing/data/utls/BillingErrorUtils.kt | 32 ++++ .../billing/data/utls/ResultWrapperUtils.kt | 55 ++++++ .../payment/di/PurchaseRepositoryModule.kt | 25 +++ 41 files changed, 629 insertions(+), 254 deletions(-) create mode 100644 app/src/accessibilityV14NoProprietaryDebug/res/values/titles.xml rename app/src/{accessibilityV27Debug => accessibilityV14ProprietaryDebug}/res/values/titles.xml (67%) create mode 100644 app/src/accessibilityV27NoProprietaryDebug/res/values/titles.xml rename app/src/{notificationListenerV18Debug => accessibilityV27ProprietaryDebug}/res/values/titles.xml (67%) rename app/src/main/java/com/app/missednotificationsreminder/{data => common/domain/entities}/ResultWrapper.kt (67%) delete mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/BillingErrorCodes.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/BillingErrorCodes.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/ConsumeResult.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/Purchase.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/PurchaseState.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuDetails.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuType.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/repository/PurchaseRepository.kt delete mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/data/model/Purchase.kt create mode 100644 app/src/main/java/com/app/missednotificationsreminder/payment/model/Purchase.kt create mode 100644 app/src/noProprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt create mode 100644 app/src/noProprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt create mode 100644 app/src/notificationListenerV18NoProprietaryDebug/res/values/titles.xml rename app/src/{accessibilityV14Debug => notificationListenerV18ProprietaryDebug}/res/values/titles.xml (67%) rename app/src/{main/java/com/app/missednotificationsreminder/payment/billing/data/source/PurchaseRepository.kt => proprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt} (56%) create mode 100644 app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/PurchaseMappers.kt create mode 100644 app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuDetailsMappers.kt create mode 100644 app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuTypeMappers.kt rename app/src/{main => proprietary}/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/BillingOperationException.kt (94%) rename app/src/{main => proprietary}/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/CoroutinesBilling.kt (100%) create mode 100644 app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/BillingErrorUtils.kt create mode 100644 app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/ResultWrapperUtils.kt create mode 100644 app/src/proprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 25d738c..727c48d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,13 @@ val propsFile = rootProject.file("keystore.properties") fun getVersionCode(minSdkVersion: Int) = 2000000000 + versionMajor * 10000000 + versionMinor * 100000 + versionPatch * 1000 + versionBuild * 100 + minSdkVersion +val proprietaryConfigurations = listOf( + "notificationListenerV18ProprietaryImplementation", + "notificationListenerV27ProprietaryImplementation", + "accessibilityV14ProprietaryImplementation", + "accessibilityV27ProprietaryImplementation" +) + android { compileSdkVersion(Constants.COMPILE_SDK_VERSION) buildToolsVersion = Constants.BUILD_TOOLS_VERSION @@ -113,15 +120,15 @@ android { } } } - flavorDimensions("service", "api") + flavorDimensions("service", "api", "license") productFlavors { create("accessibility") { dimension = "service" - versionCode = 0 + versionCode = 1 } create("notificationListener") { dimension = "service" - versionCode = 1 + versionCode = 2 } create("v14") { minSdkVersion(14) @@ -138,6 +145,26 @@ android { dimension = "api" versionCode = 3 } + create("Proprietary") { + dimension = "license" + } + create("NoProprietary") { + dimension = "license" + } + } + + + configurations { + proprietaryConfigurations.forEach { create(it) } + } + + sourceSets { + create("notificationListenerV27Proprietary") { + manifest.srcFile("src/notificationListenerV27/AndroidManifest.xml") + } + create("notificationListenerV27NoProprietary") { + manifest.srcFile("src/notificationListenerV27/AndroidManifest.xml") + } } lintOptions { @@ -176,11 +203,6 @@ android { } } -configurations.onEach { - it.resolutionStrategy { - } -} - dependencies { implementation(kotlin("stdlib-jdk7", KotlinCompilerVersion.VERSION)) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.COROUTINES}") @@ -199,7 +221,6 @@ dependencies { implementation("androidx.lifecycle:lifecycle-livedata-ktx:${Versions.LIFECYCLE}") implementation("androidx.lifecycle:lifecycle-service:${Versions.LIFECYCLE}") implementation("com.google.android.material:material:1.3.0") - implementation("com.android.billingclient:billing:3.0.2") implementation("androidx.navigation:navigation-fragment-ktx:${Versions.NAVIGATION}") implementation("androidx.navigation:navigation-ui-ktx:${Versions.NAVIGATION}") @@ -234,6 +255,11 @@ dependencies { implementation("androidx.work:work-runtime:${Versions.WORK}") implementation("androidx.work:work-runtime-ktx:${Versions.WORK}") + val proprietaryDependencies = listOf("com.android.billingclient:billing:3.0.2") + proprietaryConfigurations.forEach { configuration -> + proprietaryDependencies.forEach { dependency -> add(configuration, dependency) } + } + androidTestImplementation("junit:junit:4.13") androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0") androidTestImplementation("androidx.test:runner:1.3.0") diff --git a/app/lint.xml b/app/lint.xml index 30db547..b0087c0 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -2,7 +2,11 @@ + + + - \ No newline at end of file + diff --git a/app/src/accessibilityV14NoProprietaryDebug/res/values/titles.xml b/app/src/accessibilityV14NoProprietaryDebug/res/values/titles.xml new file mode 100644 index 0000000..e509f1c --- /dev/null +++ b/app/src/accessibilityV14NoProprietaryDebug/res/values/titles.xml @@ -0,0 +1,7 @@ + + + + Missed Notifications Reminder Av14NP Debug + Missed Notifications Reminder Av14NP (D) + + diff --git a/app/src/accessibilityV27Debug/res/values/titles.xml b/app/src/accessibilityV14ProprietaryDebug/res/values/titles.xml similarity index 67% rename from app/src/accessibilityV27Debug/res/values/titles.xml rename to app/src/accessibilityV14ProprietaryDebug/res/values/titles.xml index 6934715..ec21ed8 100644 --- a/app/src/accessibilityV27Debug/res/values/titles.xml +++ b/app/src/accessibilityV14ProprietaryDebug/res/values/titles.xml @@ -1,7 +1,7 @@ - Missed Notifications Reminder Av27 Debug - Missed Notifications Reminder Av27 (D) + Missed Notifications Reminder Av14P Debug + Missed Notifications Reminder Av14P (D) - \ No newline at end of file + diff --git a/app/src/accessibilityV27NoProprietaryDebug/res/values/titles.xml b/app/src/accessibilityV27NoProprietaryDebug/res/values/titles.xml new file mode 100644 index 0000000..ef8b303 --- /dev/null +++ b/app/src/accessibilityV27NoProprietaryDebug/res/values/titles.xml @@ -0,0 +1,7 @@ + + + + Missed Notifications Reminder Av27NP Debug + Missed Notifications Reminder Av27NP (D) + + diff --git a/app/src/notificationListenerV18Debug/res/values/titles.xml b/app/src/accessibilityV27ProprietaryDebug/res/values/titles.xml similarity index 67% rename from app/src/notificationListenerV18Debug/res/values/titles.xml rename to app/src/accessibilityV27ProprietaryDebug/res/values/titles.xml index e47490a..5238eca 100644 --- a/app/src/notificationListenerV18Debug/res/values/titles.xml +++ b/app/src/accessibilityV27ProprietaryDebug/res/values/titles.xml @@ -1,7 +1,7 @@ - Missed Notifications Reminder v18 Debug - Missed Notifications Reminder v18 (D) + Missed Notifications Reminder Av27P Debug + Missed Notifications Reminder Av27P (D) - \ No newline at end of file + diff --git a/app/src/main/java/com/app/missednotificationsreminder/data/ResultWrapper.kt b/app/src/main/java/com/app/missednotificationsreminder/common/domain/entities/ResultWrapper.kt similarity index 67% rename from app/src/main/java/com/app/missednotificationsreminder/data/ResultWrapper.kt rename to app/src/main/java/com/app/missednotificationsreminder/common/domain/entities/ResultWrapper.kt index bdc4acf..fe586c2 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/data/ResultWrapper.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/common/domain/entities/ResultWrapper.kt @@ -1,10 +1,7 @@ -package com.app.missednotificationsreminder.data +package com.app.missednotificationsreminder.common.domain.entities -import com.app.missednotificationsreminder.data.ResultWrapper.Error -import com.app.missednotificationsreminder.data.ResultWrapper.Success -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.fold -import kotlinx.coroutines.flow.transformWhile +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper.Error +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper.Success /** * A generic class that holds a value or error @@ -13,7 +10,11 @@ import kotlinx.coroutines.flow.transformWhile sealed class ResultWrapper { data class Success(val data: T) : ResultWrapper() - data class Error(val throwable: Throwable? = null, val code: Int = 0, val message: String? = null) : + data class Error( + val throwable: Throwable? = null, + val code: Int = 0, + val message: String? = null + ) : ResultWrapper() { fun messageOrDefault(defaultValue: () -> String): String = message ?: defaultValue() } @@ -35,7 +36,8 @@ val ResultWrapper<*>.succeeded /** * Map [Result] to [ResultWrapper] */ -fun Result.asResultWrapper(): ResultWrapper = if (isFailure) Error(exceptionOrNull()) else Success(getOrNull()!!) +fun Result.asResultWrapper(): ResultWrapper = + if (isFailure) Error(exceptionOrNull()) else Success(getOrNull()!!) /** * Returns the encapsulated result of the given [transform] function applied to the encapsulated value @@ -116,40 +118,3 @@ inline fun ResultWrapper.fold( is Error -> onFailure(this@fold) } } - -/** - * Collect the `Flow` results until last or [Error] value is emitted. Collect - * data using [collector]. - * - * Note, that this function rethrows any [Throwable] exception thrown by [collector] function. - */ -suspend fun Flow>.collectWithLastErrorOrSuccessStatusSimple( - defaultValue: ResultWrapper, - collector: (R, T) -> R): ResultWrapper { - return collectWithLastErrorOrSuccessStatus( - defaultValue, - { it.succeeded }) - { mergedValue, value -> - value.map { - val mergedData = (mergedValue as Success).data - collector(mergedData, it) - } - } -} - -/** - * Collect the `Flow` results until last or not succeeded value is emitted. Collect - * data using [collector]. - * - * Note, that this function rethrows any [Throwable] exception thrown by [collector] function. - */ -suspend fun Flow.collectWithLastErrorOrSuccessStatus( - defaultValue: R, - succeededTest: (T) -> Boolean, - collector: suspend (R, T) -> R): R { - return transformWhile { - emit(it) - succeededTest(it) - } - .fold(defaultValue, collector) -} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseAdapter.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseAdapter.kt index bd1710d..cfe3f07 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseAdapter.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.lifecycle.* import androidx.recyclerview.widget.RecyclerView -import com.app.missednotificationsreminder.data.onSuccess +import com.app.missednotificationsreminder.common.domain.entities.onSuccess import com.app.missednotificationsreminder.databinding.ItemContributeBinding import com.app.missednotificationsreminder.databinding.ItemDonateBinding import com.app.missednotificationsreminder.ui.widget.recyclerview.LifecycleAdapterWithViewEffect @@ -115,4 +115,4 @@ class PurchaseAdapter @Inject constructor(private val purchaseViewState: StateFl binding.viewState = item.asLiveData() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItem.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItem.kt index 338c280..ae4ab02 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItem.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItem.kt @@ -1,8 +1,8 @@ package com.app.missednotificationsreminder.payment -import com.android.billingclient.api.SkuDetails +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuDetails /** * The class to store purchase item information */ -data class PurchaseItem(val skuDetails: SkuDetails) \ No newline at end of file +data class PurchaseItem(val skuDetails: SkuDetails) diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItemViewState.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItemViewState.kt index 048322a..974ae9b 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItemViewState.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseItemViewState.kt @@ -1,10 +1,10 @@ package com.app.missednotificationsreminder.payment -import com.android.billingclient.api.SkuDetails +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuDetails /** * The class to store purchase item view state information */ data class PurchaseItemViewState(val skuDetails: SkuDetails) { val price: String = skuDetails.price -} \ No newline at end of file +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewEffect.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewEffect.kt index 41e4757..d586b6d 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewEffect.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewEffect.kt @@ -1,6 +1,6 @@ package com.app.missednotificationsreminder.payment -import com.android.billingclient.api.SkuDetails +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuDetails sealed class PurchaseViewEffect { /** @@ -12,4 +12,4 @@ sealed class PurchaseViewEffect { * Show the specified [message] */ data class Message(val message: String) : PurchaseViewEffect() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewModel.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewModel.kt index 8f1983f..8159160 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewModel.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewModel.kt @@ -2,22 +2,21 @@ package com.app.missednotificationsreminder.payment import android.app.Activity import androidx.lifecycle.viewModelScope -import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.SkuDetails import com.app.missednotificationsreminder.R import com.app.missednotificationsreminder.binding.model.BaseViewStateViewEffectModel import com.app.missednotificationsreminder.binding.util.bindWithPreferences -import com.app.missednotificationsreminder.data.* +import com.app.missednotificationsreminder.common.domain.entities.* import com.app.missednotificationsreminder.data.source.ResourceDataSource -import com.app.missednotificationsreminder.payment.billing.data.source.BillingErrorCodes -import com.app.missednotificationsreminder.payment.billing.data.source.PurchaseRepository -import com.app.missednotificationsreminder.payment.data.model.Purchase +import com.app.missednotificationsreminder.payment.billing.domain.entities.BillingErrorCodes +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuDetails +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuType +import com.app.missednotificationsreminder.payment.billing.domain.repository.PurchaseRepository import com.app.missednotificationsreminder.payment.di.qualifiers.AvailableSkus +import com.app.missednotificationsreminder.payment.model.Purchase import com.app.missednotificationsreminder.util.loadingstate.HasLoadingStateManager import com.app.missednotificationsreminder.util.loadingstate.LoadingState import com.app.missednotificationsreminder.util.loadingstate.LoadingStateManager import com.tfcporciuncula.flow.Preference -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber @@ -28,8 +27,6 @@ import kotlin.time.ExperimentalTime /** * The view model for the applications selection view */ -@ExperimentalCoroutinesApi -@OptIn(ExperimentalTime::class) class PurchaseViewModel @Inject constructor( @param:AvailableSkus private val skus: List, private val purchaseRepository: PurchaseRepository, @@ -37,7 +34,8 @@ class PurchaseViewModel @Inject constructor( private val resourcesDataSource: ResourceDataSource, ) : BaseViewStateViewEffectModel( - PurchaseViewState(contributeOptions = resourcesDataSource.getString(R.string.contribution_contribute_options))), + PurchaseViewState(contributeOptions = resourcesDataSource.getString(R.string.contribution_contribute_options)) + ), ObservesPendingPayments by ObservesPendingPaymentsImpl(purchaseRepository, purchases), HasLoadingStateManager { init { @@ -55,8 +53,7 @@ class PurchaseViewModel @Inject constructor( override var loadingState: LoadingState get() = viewState.value.loadingState set(value) { - processSync(PurchaseViewStatePartialChanges.LoadingStateChange( - value)) + processSync(PurchaseViewStatePartialChanges.LoadingStateChange(value)) } } @@ -80,7 +77,7 @@ class PurchaseViewModel @Inject constructor( attachLoadingStatus(resourcesDataSource.getString(R.string.payment_loading_purchase_items)) { purchaseRepository.verifyAndConsumePendingPurchases() .also { purchaseCompleted(it.skuDetails) } - purchaseRepository.getSkuDetails(skus, BillingClient.SkuType.INAPP) + purchaseRepository.getSkuDetails(skus, SkuType.INAPP) .map { skuDetails -> skuDetails .asSequence() @@ -111,10 +108,16 @@ class PurchaseViewModel @Inject constructor( .operationStatus } .fold( - { - requestViewEffect(PurchaseViewEffect.Message(resourcesDataSource.getString(R.string.payment_purchase_done))) + onSuccess = { + requestViewEffect( + PurchaseViewEffect.Message( + resourcesDataSource.getString( + R.string.payment_purchase_done + ) + ) + ) }, - { error -> + onFailure = { error -> if (canRetryPendingPayments(error)) { viewModelScope.launch { observePendingPayments() } } @@ -129,7 +132,11 @@ class PurchaseViewModel @Inject constructor( @ExperimentalTime interface ObservesPendingPayments { - suspend fun observePendingPayments(initialDelay: Long = DurationUnit.MINUTES.toMillis(3), interval: Long = DurationUnit.MINUTES.toMillis(3)) + suspend fun observePendingPayments( + initialDelay: Long = DurationUnit.MINUTES.toMillis(3), + interval: Long = DurationUnit.MINUTES.toMillis(3) + ) + fun canRetryPendingPayments(error: ResultWrapper.Error): Boolean fun purchaseCompleted(purchasedGoods: List) } @@ -137,7 +144,8 @@ interface ObservesPendingPayments { @ExperimentalTime class ObservesPendingPaymentsImpl( private val purchaseRepository: PurchaseRepository, - private val purchases: Preference>) : ObservesPendingPayments { + private val purchases: Preference> +) : ObservesPendingPayments { override suspend fun observePendingPayments(initialDelay: Long, interval: Long) { Timber.d("observePendingPayments() called with: initialDelay = $initialDelay, interval = $interval") delay(initialDelay) @@ -169,4 +177,4 @@ class ObservesPendingPaymentsImpl( .takeIf { it.isNotEmpty() } ?.run { purchases.set(purchases.get() + this@run) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewState.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewState.kt index 3a12b27..129ce1a 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewState.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewState.kt @@ -1,15 +1,17 @@ package com.app.missednotificationsreminder.payment -import com.app.missednotificationsreminder.data.ResultWrapper +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper import com.app.missednotificationsreminder.util.loadingstate.LoadingState -data class PurchaseViewState(val loadingState: LoadingState = LoadingState(), - val data: ResultWrapper>? = null, - val purchases: String = "", - val contributeOptions: String) { +data class PurchaseViewState( + val loadingState: LoadingState = LoadingState(), + val data: ResultWrapper>? = null, + val purchases: String = "", + val contributeOptions: String +) { val error = if (data is ResultWrapper.Error) data.messageOrDefault { "Not specified" } else "None" val errorVisible = data is ResultWrapper.Error val purchasesVisible: Boolean get() = purchases.isNotEmpty() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewStatePartialChanges.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewStatePartialChanges.kt index 6d04d85..d4d4edc 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewStatePartialChanges.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/PurchaseViewStatePartialChanges.kt @@ -2,10 +2,10 @@ package com.app.missednotificationsreminder.payment import com.app.missednotificationsreminder.R import com.app.missednotificationsreminder.binding.model.ViewStatePartialChanges -import com.app.missednotificationsreminder.data.ResultWrapper +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper import com.app.missednotificationsreminder.data.source.ResourceDataSource +import com.app.missednotificationsreminder.payment.model.Purchase import com.app.missednotificationsreminder.util.loadingstate.LoadingState -import com.app.missednotificationsreminder.payment.data.model.Purchase sealed class PurchaseViewStatePartialChanges : ViewStatePartialChanges { data class LoadingStateChange( @@ -16,7 +16,8 @@ sealed class PurchaseViewStatePartialChanges : ViewStatePartialChanges>) : PurchaseViewStatePartialChanges() { + private val newValue: ResultWrapper> + ) : PurchaseViewStatePartialChanges() { override fun reduce(previousState: PurchaseViewState): PurchaseViewState { return previousState.copy(data = newValue) } @@ -30,4 +31,4 @@ sealed class PurchaseViewStatePartialChanges : ViewStatePartialChanges - * @return - */ - fun handleBillingError(error: Throwable?, resourceDataSource: ResourceDataSource): ResultWrapper { - if (error is BillingOperationException) { - when (error.code) { - BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> - return ResultWrapper.Error(error, BILLING_UNAVAILABLE, resourceDataSource.getString(R.string.payment_error_billing_unavailable)) - BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> - return ResultWrapper.Error(error, SERVICE_UNAVAILABLE, resourceDataSource.getString(R.string.payment_error_service_unavailable)) - BillingClient.BillingResponseCode.USER_CANCELED -> - return ResultWrapper.Error(error, USER_CANCELED, resourceDataSource.getString(R.string.payment_error_user_canceled)) - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> - return ResultWrapper.Error(error, ITEM_ALREADY_OWNED, resourceDataSource.getString(R.string.payment_error_purchase_pending)) - } - } - return ResultWrapper.Error(error) - } - - fun ResultWrapper.Error.handleBillingError(resourceDataSource: ResourceDataSource): ResultWrapper { - return handleBillingError(throwable, resourceDataSource) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/BillingErrorCodes.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/BillingErrorCodes.kt new file mode 100644 index 0000000..fd2113a --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/BillingErrorCodes.kt @@ -0,0 +1,13 @@ +package com.app.missednotificationsreminder.payment.billing.domain.entities + +object BillingErrorCodes { + const val BILLING_UNAVAILABLE = 3 + const val SERVICE_UNAVAILABLE = 2 + const val ITEM_ALREADY_OWNED = 7 + const val USER_CANCELED = 1 + + /** + * The purchase payment is pending + */ + const val PURCHASE_PENDING = 10 +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/ConsumeResult.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/ConsumeResult.kt new file mode 100644 index 0000000..92dcbaf --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/ConsumeResult.kt @@ -0,0 +1,5 @@ +package com.app.missednotificationsreminder.payment.billing.domain.entities + +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper + +data class ConsumeResult(val skuDetails: List, val operationStatus: ResultWrapper) diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/Purchase.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/Purchase.kt new file mode 100644 index 0000000..b2de48b --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/Purchase.kt @@ -0,0 +1,8 @@ +package com.app.missednotificationsreminder.payment.billing.domain.entities + +data class Purchase( + val sku: String, + val purchaseToken: String, + val isAcknowledged: Boolean, + val purchaseState: PurchaseState, +) diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/PurchaseState.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/PurchaseState.kt new file mode 100644 index 0000000..a808040 --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/PurchaseState.kt @@ -0,0 +1,7 @@ +package com.app.missednotificationsreminder.payment.billing.domain.entities + +enum class PurchaseState { + UNSPECIFIED_STATE, + PURCHASED, + PENDING, +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuDetails.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuDetails.kt new file mode 100644 index 0000000..2bd12f2 --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuDetails.kt @@ -0,0 +1,8 @@ +package com.app.missednotificationsreminder.payment.billing.domain.entities + +data class SkuDetails( + val sku: String, + val price: String, + val priceAmountMicros: Long, + val skuType: SkuType, +) diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuType.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuType.kt new file mode 100644 index 0000000..735034b --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/entities/SkuType.kt @@ -0,0 +1,6 @@ +package com.app.missednotificationsreminder.payment.billing.domain.entities + +enum class SkuType { + INAPP, + SUBS, +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/repository/PurchaseRepository.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/repository/PurchaseRepository.kt new file mode 100644 index 0000000..abfdfa5 --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/billing/domain/repository/PurchaseRepository.kt @@ -0,0 +1,49 @@ +package com.app.missednotificationsreminder.payment.billing.domain.repository + +import android.app.Activity +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper +import com.app.missednotificationsreminder.payment.billing.domain.entities.ConsumeResult +import com.app.missednotificationsreminder.payment.billing.domain.entities.Purchase +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuDetails +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuType + +interface PurchaseRepository { + /** + * Get the sku details for the specified [skuList] of the [skuType] product type + */ + suspend fun getSkuDetails( + skuList: List, + skuType: SkuType + ): ResultWrapper> + + /** + * Launch the purchase flow for the specified product details + */ + suspend fun purchase( + skuDetails: SkuDetails, + oldSku: String? = null, + oldPurchaseToken: String? = null, + userId: String? = null, + activity: Activity + ): ResultWrapper> + + /** + * Query purchases for the specified [skuType] + */ + suspend fun queryPurchases(skuType: SkuType): ResultWrapper> + + /** + * Acknowledge [purchase] to avoid purchase transaction rollback + */ + suspend fun acknowledgePurchase(purchase: Purchase): ResultWrapper + + /** + * Consume the [purchase] so user may but it again + */ + suspend fun consumePurchase(purchase: Purchase): ResultWrapper + + /** + * Check whether there are any unhandled purchases and try to handle them + */ + suspend fun verifyAndConsumePendingPurchases(): ConsumeResult +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/data/model/Purchase.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/data/model/Purchase.kt deleted file mode 100644 index 86330ef..0000000 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/data/model/Purchase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.app.missednotificationsreminder.payment.data.model - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Purchase(val sku: String, val price: String) \ No newline at end of file diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/di/PurchaseDataModule.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/di/PurchaseDataModule.kt index 5b712a2..f77a5f7 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/di/PurchaseDataModule.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/di/PurchaseDataModule.kt @@ -1,10 +1,6 @@ package com.app.missednotificationsreminder.payment.di -import android.content.Context -import com.app.missednotificationsreminder.data.source.ResourceDataSource -import com.app.missednotificationsreminder.di.qualifiers.ForApplication -import com.app.missednotificationsreminder.payment.billing.data.source.PurchaseRepository -import com.app.missednotificationsreminder.payment.data.model.Purchase +import com.app.missednotificationsreminder.payment.model.Purchase import com.app.missednotificationsreminder.util.moshi.MoshiPreferenceWrapper import com.squareup.moshi.Moshi import com.squareup.moshi.Types @@ -12,22 +8,19 @@ import com.tfcporciuncula.flow.FlowSharedPreferences import com.tfcporciuncula.flow.Preference import dagger.Module import dagger.Provides -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import javax.inject.Singleton -@Module(includes = [PurchaseDataModuleExt::class]) +@Module(includes = [PurchaseDataModuleExt::class, PurchaseRepositoryModule::class]) class PurchaseDataModule { - @Provides - @Singleton - fun providePurchaseRepository(@ForApplication context: Context, resourceDataSource: ResourceDataSource) = PurchaseRepository(CoroutineScope(Dispatchers.Main), resourceDataSource, context) - @Provides @Singleton fun providePurchases(prefs: FlowSharedPreferences, moshi: Moshi): Preference> { - return MoshiPreferenceWrapper(prefs, "PURCHASES", + return MoshiPreferenceWrapper( + prefs, + "PURCHASES", emptyList(), - moshi.adapter(Types.newParameterizedType(List::class.java, Purchase::class.java))) + moshi.adapter(Types.newParameterizedType(List::class.java, Purchase::class.java)) + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/model/Purchase.kt b/app/src/main/java/com/app/missednotificationsreminder/payment/model/Purchase.kt new file mode 100644 index 0000000..3bbc271 --- /dev/null +++ b/app/src/main/java/com/app/missednotificationsreminder/payment/model/Purchase.kt @@ -0,0 +1,6 @@ +package com.app.missednotificationsreminder.payment.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Purchase(val sku: String, val price: String) diff --git a/app/src/main/java/com/app/missednotificationsreminder/service/ReminderNotificationListenerService.kt b/app/src/main/java/com/app/missednotificationsreminder/service/ReminderNotificationListenerService.kt index 7e6d4f2..c06197a 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/service/ReminderNotificationListenerService.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/service/ReminderNotificationListenerService.kt @@ -38,7 +38,7 @@ import androidx.work.WorkManager import androidx.work.await import com.app.missednotificationsreminder.R import com.app.missednotificationsreminder.di.Injector.Companion.obtain -import com.app.missednotificationsreminder.payment.data.model.Purchase +import com.app.missednotificationsreminder.payment.model.Purchase import com.app.missednotificationsreminder.service.data.model.NotificationData import com.app.missednotificationsreminder.service.event.NotificationsUpdatedEvent import com.app.missednotificationsreminder.service.event.RemindEvents @@ -67,7 +67,6 @@ import javax.inject.Inject * there are available notifications from applications which matches user selected applications. The notification interval * is also specified by the user in the corresponding window. */ -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) class ReminderNotificationListenerService : AbstractReminderNotificationListenerService() { private val mDispatcher = ServiceLifecycleDispatcher(this) diff --git a/app/src/main/java/com/app/missednotificationsreminder/settings/SettingsViewModel.kt b/app/src/main/java/com/app/missednotificationsreminder/settings/SettingsViewModel.kt index 008d159..085b077 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/settings/SettingsViewModel.kt @@ -15,8 +15,8 @@ import com.app.missednotificationsreminder.binding.util.bindWithPreferences import com.app.missednotificationsreminder.data.model.NightMode import com.app.missednotificationsreminder.payment.ObservesPendingPayments import com.app.missednotificationsreminder.payment.ObservesPendingPaymentsImpl -import com.app.missednotificationsreminder.payment.billing.data.source.PurchaseRepository -import com.app.missednotificationsreminder.payment.data.model.Purchase +import com.app.missednotificationsreminder.payment.billing.domain.repository.PurchaseRepository +import com.app.missednotificationsreminder.payment.model.Purchase import com.app.missednotificationsreminder.service.ReminderNotificationListenerService import com.app.missednotificationsreminder.service.util.ReminderNotificationListenerServiceUtils import com.app.missednotificationsreminder.settings.di.qualifiers.ForceWakeLock @@ -32,12 +32,14 @@ import javax.inject.Inject * The view model for the settings view */ @ExperimentalCoroutinesApi -class SettingsViewModel @Inject constructor(private val vibrator: Vibrator, - private val nightMode: Preference, - @param:ForceWakeLock private val forceWakeLock: Preference, - @param:RateAppClicked private val rateAppClicked: Preference, - private val purchaseRepository: PurchaseRepository, - private val purchases: Preference>) : +class SettingsViewModel @Inject constructor( + private val vibrator: Vibrator, + private val nightMode: Preference, + @param:ForceWakeLock private val forceWakeLock: Preference, + @param:RateAppClicked private val rateAppClicked: Preference, + private val purchaseRepository: PurchaseRepository, + private val purchases: Preference> +) : BaseViewStateModel(SettingsViewState()), ObservesPendingPayments by ObservesPendingPaymentsImpl(purchaseRepository, purchases) { @@ -66,7 +68,10 @@ class SettingsViewModel @Inject constructor(private val vibrator: Vibrator, * Run the operation to check whether the notification service is enabled */ fun checkServiceEnabled(context: Context) { - ReminderNotificationListenerServiceUtils.isServiceEnabled(context, ReminderNotificationListenerService::class.java) + ReminderNotificationListenerServiceUtils.isServiceEnabled( + context, + ReminderNotificationListenerService::class.java + ) .run { process(SettingsViewStatePartialChanges.AccessEnabledChange(this)) } @@ -116,16 +121,22 @@ class SettingsViewModel @Inject constructor(private val vibrator: Vibrator, val goToMarket = Intent(Intent.ACTION_VIEW, uri) // To count with Play market backstack, After pressing back button, // to taken back to our application, we need to add following flags to intent. - goToMarket.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or - Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + goToMarket.addFlags( + Intent.FLAG_ACTIVITY_NO_HISTORY or + Intent.FLAG_ACTIVITY_MULTIPLE_TASK + ) if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { goToMarket.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) } try { activity.startActivity(goToMarket) } catch (e: ActivityNotFoundException) { - activity.startActivity(Intent(Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/apps/details?id=${activity.packageName}"))) + activity.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=${activity.packageName}") + ) + ) } } @@ -136,6 +147,7 @@ class SettingsViewModel @Inject constructor(private val vibrator: Vibrator, val REQUIRED_PERMISSIONS = listOf( Manifest.permission.WAKE_LOCK, Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.VIBRATE) + Manifest.permission.VIBRATE + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/util/coroutines/CoroutinesExtensions.kt b/app/src/main/java/com/app/missednotificationsreminder/util/coroutines/CoroutinesExtensions.kt index 5c4b4ff..5bc51f8 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/util/coroutines/CoroutinesExtensions.kt +++ b/app/src/main/java/com/app/missednotificationsreminder/util/coroutines/CoroutinesExtensions.kt @@ -1,7 +1,7 @@ package com.app.missednotificationsreminder.util.coroutines -import com.app.missednotificationsreminder.data.ResultWrapper -import com.app.missednotificationsreminder.data.asResultWrapper +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper +import com.app.missednotificationsreminder.common.domain.entities.asResultWrapper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -46,4 +46,4 @@ suspend fun retryCallOnError(maxRetryCount: Int, block: suspend () -> T): Re Timber.d("retryCallOnError: don't retry, max retry count reached") } return result.asResultWrapper() -} \ No newline at end of file +} diff --git a/app/src/noProprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt b/app/src/noProprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt new file mode 100644 index 0000000..ee236dd --- /dev/null +++ b/app/src/noProprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt @@ -0,0 +1,52 @@ +package com.app.missednotificationsreminder.payment.billing.data + +import android.app.Activity +import com.app.missednotificationsreminder.R +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper +import com.app.missednotificationsreminder.data.source.ResourceDataSource +import com.app.missednotificationsreminder.payment.billing.domain.entities.* +import com.app.missednotificationsreminder.payment.billing.domain.repository.PurchaseRepository + +class PurchaseRepositoryImpl( + resourceDataSource: ResourceDataSource +) : PurchaseRepository { + + private val defaultAnswer = ResultWrapper.Error( + throwable = null, + code = BillingErrorCodes.BILLING_UNAVAILABLE, + message = resourceDataSource.getString(R.string.payment_error_billing_unavailable) + ) + + override suspend fun getSkuDetails( + skuList: List, + skuType: SkuType + ): ResultWrapper> { + return defaultAnswer + } + + override suspend fun purchase( + skuDetails: SkuDetails, + oldSku: String?, + oldPurchaseToken: String?, + userId: String?, + activity: Activity + ): ResultWrapper> { + return defaultAnswer + } + + override suspend fun queryPurchases(skuType: SkuType): ResultWrapper> { + return defaultAnswer + } + + override suspend fun acknowledgePurchase(purchase: Purchase): ResultWrapper { + return defaultAnswer + } + + override suspend fun consumePurchase(purchase: Purchase): ResultWrapper { + return defaultAnswer + } + + override suspend fun verifyAndConsumePendingPurchases(): ConsumeResult { + return ConsumeResult(skuDetails = emptyList(), operationStatus = defaultAnswer) + } +} diff --git a/app/src/noProprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt b/app/src/noProprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt new file mode 100644 index 0000000..6bbfc03 --- /dev/null +++ b/app/src/noProprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt @@ -0,0 +1,20 @@ +package com.app.missednotificationsreminder.payment.di + +import com.app.missednotificationsreminder.data.source.ResourceDataSource +import com.app.missednotificationsreminder.payment.billing.data.PurchaseRepositoryImpl +import com.app.missednotificationsreminder.payment.billing.domain.repository.PurchaseRepository +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class PurchaseRepositoryModule { + + @Provides + @Singleton + fun providePurchaseRepository( + resourceDataSource: ResourceDataSource + ): PurchaseRepository { + return PurchaseRepositoryImpl(resourceDataSource) + } +} diff --git a/app/src/notificationListenerV18NoProprietaryDebug/res/values/titles.xml b/app/src/notificationListenerV18NoProprietaryDebug/res/values/titles.xml new file mode 100644 index 0000000..69943b9 --- /dev/null +++ b/app/src/notificationListenerV18NoProprietaryDebug/res/values/titles.xml @@ -0,0 +1,7 @@ + + + + Missed Notifications Reminder v18NP Debug + Missed Notifications Reminder v18NP (D) + + diff --git a/app/src/accessibilityV14Debug/res/values/titles.xml b/app/src/notificationListenerV18ProprietaryDebug/res/values/titles.xml similarity index 67% rename from app/src/accessibilityV14Debug/res/values/titles.xml rename to app/src/notificationListenerV18ProprietaryDebug/res/values/titles.xml index bffe6ac..f4648c2 100644 --- a/app/src/accessibilityV14Debug/res/values/titles.xml +++ b/app/src/notificationListenerV18ProprietaryDebug/res/values/titles.xml @@ -1,7 +1,7 @@ - Missed Notifications Reminder Av14 Debug - Missed Notifications Reminder Av14 (D) + Missed Notifications Reminder v18P Debug + Missed Notifications Reminder v18P (D) - \ No newline at end of file + diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/PurchaseRepository.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt similarity index 56% rename from app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/PurchaseRepository.kt rename to app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt index 1676fa6..7ad6806 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/PurchaseRepository.kt +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/PurchaseRepositoryImpl.kt @@ -1,17 +1,21 @@ -package com.app.missednotificationsreminder.payment.billing.data.source +package com.app.missednotificationsreminder.payment.billing.data import android.app.Activity import android.content.Context import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.BillingClient.SkuType -import com.android.billingclient.api.Purchase -import com.android.billingclient.api.SkuDetails import com.app.missednotificationsreminder.R +import com.app.missednotificationsreminder.common.domain.entities.* import com.app.missednotificationsreminder.data.* import com.app.missednotificationsreminder.data.source.ResourceDataSource -import com.app.missednotificationsreminder.payment.billing.data.source.BillingErrorCodes.handleBillingError +import com.app.missednotificationsreminder.payment.billing.data.mappers.toDomain +import com.app.missednotificationsreminder.payment.billing.data.mappers.toRemote import com.app.missednotificationsreminder.payment.billing.data.source.remote.BillingOperationException import com.app.missednotificationsreminder.payment.billing.data.source.remote.CoroutinesBilling +import com.app.missednotificationsreminder.payment.billing.data.utls.collectWithLastErrorOrSuccessStatus +import com.app.missednotificationsreminder.payment.billing.data.utls.collectWithLastErrorOrSuccessStatusSimple +import com.app.missednotificationsreminder.payment.billing.data.utls.handleBillingError +import com.app.missednotificationsreminder.payment.billing.domain.entities.* +import com.app.missednotificationsreminder.payment.billing.domain.repository.PurchaseRepository import com.app.missednotificationsreminder.util.coroutines.retryCallOnError import com.app.missednotificationsreminder.util.loadingstate.BasicLoadingStateManager import com.app.missednotificationsreminder.util.loadingstate.HasLoadingStateManager @@ -21,22 +25,24 @@ import kotlinx.coroutines.flow.* import timber.log.Timber import kotlin.coroutines.EmptyCoroutineContext -class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineContext), - private val resourceDataSource: ResourceDataSource, - context: Context) : - HasLoadingStateManager { +class PurchaseRepositoryImpl( + scope: CoroutineScope = CoroutineScope(EmptyCoroutineContext), + private val resourceDataSource: ResourceDataSource, + context: Context +) : HasLoadingStateManager, PurchaseRepository { val billing by lazy { CoroutinesBilling(scope, context) } override val loadingStateManager: LoadingStateManager by lazy { BasicLoadingStateManager() } private suspend fun processBillingCall( operationName: String, maxRetryCount: Int, - block: suspend () -> T): ResultWrapper { + block: suspend () -> T + ): ResultWrapper { return attachLoading(operationName) { retryCallOnError(maxRetryCount) { block() - }.onErrorReturn { - it.handleBillingError(resourceDataSource) + }.onErrorReturn { error -> + error.handleBillingError(resourceDataSource) } } } @@ -44,17 +50,27 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo /** * Get the sku details for the specified [skuList] of the [skuType] product type */ - suspend fun getSkuDetails(skuList: List, @BillingClient.SkuType skuType: String): ResultWrapper> { - Timber.d("getSkuDetails() called with: skuList = %s; skuType = %s", - skuList, skuType) + override suspend fun getSkuDetails( + skuList: List, + skuType: SkuType + ): ResultWrapper> { + Timber.d( + "getSkuDetails() called with: skuList = %s; skuType = %s", + skuList, skuType + ) return attachLoading("getSkuListDetails") { attachLoadingStatus("Loading tariffs information") { skuList.asFlow() .buffer() - .map { processBillingCall("getSkuDetails", 2) { billing.getSkuDetails(listOf(it), skuType) } } + .map { + processBillingCall( + "getSkuDetails", + 2 + ) { billing.getSkuDetails(listOf(it), skuType.toRemote()) } + } .collectWithLastErrorOrSuccessStatusSimple(ResultWrapper.Success(emptyList())) { acc, value -> - acc + value + acc + value.map { it.toDomain() } } } } @@ -63,22 +79,44 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo /** * Launch the purchase flow for the specified product details */ - suspend fun purchase(skuDetails: SkuDetails, oldSku: String? = null, oldPurchaseToken: String? = null, userId: String? = null, activity: Activity): ResultWrapper> { - Timber.d("purchase() called with: skuDetails = %s; oldSku = %s; oldPurchaseToken = %s; userId = %s; activity = %s", - skuDetails, oldSku, oldPurchaseToken, userId, activity) + override suspend fun purchase( + skuDetails: SkuDetails, + oldSku: String?, + oldPurchaseToken: String?, + userId: String?, + activity: Activity + ): ResultWrapper> { + Timber.d( + "purchase() called with: skuDetails = %s; oldSku = %s; oldPurchaseToken = %s; userId = %s; activity = %s", + skuDetails, oldSku, oldPurchaseToken, userId, activity + ) return processBillingCall("purchase", 0) { try { - billing.purchase(skuDetails, oldSku, oldPurchaseToken, - userId, activity) + val remoteSkuDetails = + billing.getSkuDetails(listOf(skuDetails.sku), skuDetails.skuType.toRemote()) + billing.purchase( + remoteSkuDetails.first(), + oldSku, + oldPurchaseToken, + userId, + activity + ).map { it.toDomain() } } catch (t: Throwable) { if (oldSku != null && t is BillingOperationException && - t.code == BillingClient.BillingResponseCode.DEVELOPER_ERROR) { + t.code == BillingClient.BillingResponseCode.DEVELOPER_ERROR + ) { + val remoteSkuDetails = + billing.getSkuDetails(listOf(skuDetails.sku), skuDetails.skuType.toRemote()) // Workaround for DEVELOPER_ERROR which may happen when old purchase // has PURCHASED state but has been refunded or user is purchasing already purchased item - billing.purchase(skuDetails, - null, null, - userId, activity) + billing.purchase( + remoteSkuDetails.first(), + null, + null, + userId, + activity + ).map { it.toDomain() } } else { throw t } @@ -89,20 +127,25 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo /** * Query purchases for the specified [skuType] */ - suspend fun queryPurchases(@SkuType skuType: String): ResultWrapper> { - Timber.d("queryPurchases() called with: skuType = %s", - skuType) + override suspend fun queryPurchases(skuType: SkuType): ResultWrapper> { + Timber.d( + "queryPurchases() called with: skuType = %s", + skuType + ) return processBillingCall("purchase", 2) { - billing.queryPurchases(skuType) + billing.queryPurchases(skuType.toRemote()) + .map { it.toDomain() } } } /** * Acknowledge [purchase] to avoid purchase transaction rollback */ - suspend fun acknowledgePurchase(purchase: Purchase): ResultWrapper { - Timber.d("acknowledgePurchase() called with: purchase = %s", - purchase) + override suspend fun acknowledgePurchase(purchase: Purchase): ResultWrapper { + Timber.d( + "acknowledgePurchase() called with: purchase = %s", + purchase + ) return processBillingCall("acknowledgePurchase", 2) { billing.acknowledgePurchase(purchase.purchaseToken) true @@ -112,9 +155,11 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo /** * Consume the [purchase] so user may but it again */ - suspend fun consumePurchase(purchase: Purchase): ResultWrapper { - Timber.d("consumePurchase() called with: purchase = %s", - purchase) + override suspend fun consumePurchase(purchase: Purchase): ResultWrapper { + Timber.d( + "consumePurchase() called with: purchase = %s", + purchase + ) return processBillingCall("consumePurchase", 2) { billing.consumePurchase(purchase.purchaseToken) } @@ -123,7 +168,7 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo /** * Check whether there are any unhandled purchases and try to handle them */ - suspend fun verifyAndConsumePendingPurchases(): ConsumeResult { + override suspend fun verifyAndConsumePendingPurchases(): ConsumeResult { Timber.d("verifyAndConsumePendingPurchases() called") return attachLoading("verifyAndConsumePendingPurchases") { attachLoadingStatus("Verifying and consuming pending purchases") { @@ -134,7 +179,10 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo acknowledgePurchases(it, skuType == SkuType.INAPP, skuType) } } - .collectWithLastErrorOrSuccessStatus(ConsumeResult(emptyList(), ResultWrapper.Success(Unit)), + .collectWithLastErrorOrSuccessStatus(ConsumeResult( + emptyList(), + ResultWrapper.Success(Unit) + ), { it is ResultWrapper.Success && it.data.operationStatus.succeededOrPurchasePending() }) { acc, value -> value.fold({ consumeResult -> with(acc) { @@ -151,12 +199,14 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo }, { error -> with(acc) { // calculate new operation status for accumulated value - copy(operationStatus = operationStatus.fold( - // if accumulated operation status is success - // use last received operation status as accumulated - { error }, - // else don't overwrite accumulated operation status - { operationStatus })) + copy( + operationStatus = operationStatus.fold( + // if accumulated operation status is success + // use last received operation status as accumulated + { error }, + // else don't overwrite accumulated operation status + { operationStatus }) + ) } }) } @@ -164,13 +214,17 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo } } - private suspend fun acknowledgePurchases(purchases: List, consumable: Boolean, skuType: String): ConsumeResult { + private suspend fun acknowledgePurchases( + purchases: List, + consumable: Boolean, + skuType: SkuType + ): ConsumeResult { return purchases.asFlow() .filter { !it.isAcknowledged || consumable } .flatMapMerge(concurrency = 1) { purchase -> flow { when (purchase.purchaseState) { - Purchase.PurchaseState.PURCHASED -> { + PurchaseState.PURCHASED -> { acknowledgePurchase(purchase) .flatMap { consumePurchase(purchase) @@ -178,15 +232,22 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo } .also { emit(it) } } - Purchase.PurchaseState.PENDING -> - emit(ResultWrapper.Error(code = BillingErrorCodes.PURCHASE_PENDING, - message = resourceDataSource.getString(R.string.payment_error_purchase_pending))) + PurchaseState.PENDING -> + emit( + ResultWrapper.Error( + code = BillingErrorCodes.PURCHASE_PENDING, + message = resourceDataSource.getString(R.string.payment_error_purchase_pending) + ) + ) else -> { }// do nothing } } } - .collectWithLastErrorOrSuccessStatus(ConsumeResult(emptyList(), ResultWrapper.Success(Unit)), + .collectWithLastErrorOrSuccessStatus(ConsumeResult( + emptyList(), + ResultWrapper.Success(Unit) + ), { it.succeededOrPurchasePending() }) { acc, value -> value.fold({ acc.copy(skuDetails = acc.skuDetails + it) @@ -198,8 +259,6 @@ class PurchaseRepository(scope: CoroutineScope = CoroutineScope(EmptyCoroutineCo } -data class ConsumeResult(val skuDetails: List, val operationStatus: ResultWrapper) - private fun ResultWrapper.succeededOrPurchasePending(): Boolean { val rw = this@succeededOrPurchasePending return rw.succeeded || diff --git a/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/PurchaseMappers.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/PurchaseMappers.kt new file mode 100644 index 0000000..23f0871 --- /dev/null +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/PurchaseMappers.kt @@ -0,0 +1,19 @@ +package com.app.missednotificationsreminder.payment.billing.data.mappers + +import com.android.billingclient.api.Purchase +import com.app.missednotificationsreminder.payment.billing.domain.entities.PurchaseState + +fun Purchase.toDomain() = com.app.missednotificationsreminder.payment.billing.domain.entities.Purchase( + sku = sku, + purchaseToken = purchaseToken, + isAcknowledged = isAcknowledged, + purchaseState = purchaseState.toPurchaseStateDomain(), +) + +private fun Int.toPurchaseStateDomain(): PurchaseState { + return when (this) { + Purchase.PurchaseState.PENDING -> PurchaseState.PENDING + Purchase.PurchaseState.PURCHASED -> PurchaseState.PURCHASED + else -> PurchaseState.UNSPECIFIED_STATE + } +} diff --git a/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuDetailsMappers.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuDetailsMappers.kt new file mode 100644 index 0000000..fd4dff8 --- /dev/null +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuDetailsMappers.kt @@ -0,0 +1,20 @@ +package com.app.missednotificationsreminder.payment.billing.data.mappers + +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.SkuDetails +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuType + +fun SkuDetails.toDomain() = com.app.missednotificationsreminder.payment.billing.domain.entities.SkuDetails( + sku = sku, + skuType = type.toDomainSkuType(), + price = price, + priceAmountMicros = priceAmountMicros, +) + +private fun String.toDomainSkuType(): SkuType { + return when (this) { + BillingClient.SkuType.INAPP -> SkuType.INAPP + BillingClient.SkuType.SUBS -> SkuType.SUBS + else -> throw IllegalArgumentException("Unsupported sku type $this") + } +} diff --git a/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuTypeMappers.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuTypeMappers.kt new file mode 100644 index 0000000..8d0513d --- /dev/null +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/mappers/SkuTypeMappers.kt @@ -0,0 +1,12 @@ +package com.app.missednotificationsreminder.payment.billing.data.mappers + +import com.android.billingclient.api.BillingClient +import com.app.missednotificationsreminder.payment.billing.domain.entities.SkuType + +@BillingClient.SkuType +fun SkuType.toRemote(): String { + return when (this) { + SkuType.INAPP -> BillingClient.SkuType.INAPP + SkuType.SUBS -> BillingClient.SkuType.SUBS + } +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/BillingOperationException.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/BillingOperationException.kt similarity index 94% rename from app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/BillingOperationException.kt rename to app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/BillingOperationException.kt index 193f9a9..a6f8115 100644 --- a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/BillingOperationException.kt +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/BillingOperationException.kt @@ -4,7 +4,7 @@ import com.android.billingclient.api.BillingResult sealed class BillingOperationException(val code: Int, message: String) : Exception(message) { - constructor(billingResult: BillingResult) : this(billingResult.responseCode, billingResult.debugMessage?:"") + constructor(billingResult: BillingResult) : this(billingResult.responseCode, billingResult.debugMessage) override fun toString(): String { return StringBuilder() @@ -18,31 +18,31 @@ sealed class BillingOperationException(val code: Int, message: String) : Excepti } class AcknowledgePurchaseFailureException : BillingOperationException { - constructor(billingResult: BillingResult) : super(billingResult) - constructor(code: Int, message: String) : super(code, message) + constructor(billingResult: BillingResult) : super(billingResult) + constructor(code: Int, message: String) : super(code, message) } class ConnectionFailureException : BillingOperationException { - constructor(billingResult: BillingResult) : super(billingResult) - constructor(code: Int, message: String) : super(code, message) + constructor(billingResult: BillingResult) : super(billingResult) + constructor(code: Int, message: String) : super(code, message) } class ConsumePurchaseFailureException : BillingOperationException { - constructor(billingResult: BillingResult) : super(billingResult) - constructor(code: Int, message: String) : super(code, message) + constructor(billingResult: BillingResult) : super(billingResult) + constructor(code: Int, message: String) : super(code, message) } class PurchaseFailureException : BillingOperationException { - constructor(billingResult: BillingResult) : super(billingResult) - constructor(code: Int, message: String) : super(code, message) + constructor(billingResult: BillingResult) : super(billingResult) + constructor(code: Int, message: String) : super(code, message) } class QueryPurchaseFailureException : BillingOperationException { - constructor(billingResult: BillingResult) : super(billingResult) - constructor(code: Int, message: String) : super(code, message) + constructor(billingResult: BillingResult) : super(billingResult) + constructor(code: Int, message: String) : super(code, message) } class SkuDetailsFailureException : BillingOperationException { - constructor(billingResult: BillingResult) : super(billingResult) - constructor(code: Int, message: String) : super(code, message) -} \ No newline at end of file + constructor(billingResult: BillingResult) : super(billingResult) + constructor(code: Int, message: String) : super(code, message) +} diff --git a/app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/CoroutinesBilling.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/CoroutinesBilling.kt similarity index 100% rename from app/src/main/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/CoroutinesBilling.kt rename to app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/source/remote/CoroutinesBilling.kt diff --git a/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/BillingErrorUtils.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/BillingErrorUtils.kt new file mode 100644 index 0000000..5ab64b4 --- /dev/null +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/BillingErrorUtils.kt @@ -0,0 +1,32 @@ +package com.app.missednotificationsreminder.payment.billing.data.utls + +import com.android.billingclient.api.BillingClient +import com.app.missednotificationsreminder.R +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper +import com.app.missednotificationsreminder.data.source.ResourceDataSource +import com.app.missednotificationsreminder.payment.billing.data.source.remote.BillingOperationException +import com.app.missednotificationsreminder.payment.billing.domain.entities.BillingErrorCodes + +/** + * Handle billing error when detected + **/ +fun Throwable?.handleBillingError(resourceDataSource: ResourceDataSource): ResultWrapper { + val error = this + if (error is BillingOperationException) { + when (error.code) { + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> + return ResultWrapper.Error(error, BillingErrorCodes.BILLING_UNAVAILABLE, resourceDataSource.getString(R.string.payment_error_billing_unavailable)) + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> + return ResultWrapper.Error(error, BillingErrorCodes.SERVICE_UNAVAILABLE, resourceDataSource.getString(R.string.payment_error_service_unavailable)) + BillingClient.BillingResponseCode.USER_CANCELED -> + return ResultWrapper.Error(error, BillingErrorCodes.USER_CANCELED, resourceDataSource.getString(R.string.payment_error_user_canceled)) + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> + return ResultWrapper.Error(error, BillingErrorCodes.ITEM_ALREADY_OWNED, resourceDataSource.getString(R.string.payment_error_purchase_pending)) + } + } + return ResultWrapper.Error(error) +} + +fun ResultWrapper.Error.handleBillingError(resourceDataSource: ResourceDataSource): ResultWrapper { + return throwable.handleBillingError(resourceDataSource) +} diff --git a/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/ResultWrapperUtils.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/ResultWrapperUtils.kt new file mode 100644 index 0000000..97971bf --- /dev/null +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/billing/data/utls/ResultWrapperUtils.kt @@ -0,0 +1,55 @@ +package com.app.missednotificationsreminder.payment.billing.data.utls + +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper +import com.app.missednotificationsreminder.common.domain.entities.ResultWrapper.Error +import com.app.missednotificationsreminder.common.domain.entities.map +import com.app.missednotificationsreminder.common.domain.entities.succeeded +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.fold +import kotlinx.coroutines.flow.transformWhile + + +/** + * Collect the `Flow` results until last or [Error] value is emitted. Collect + * data using [collector]. + * + * Note, that this function rethrows any [Throwable] exception thrown by [collector] function. + */ +/** + * Collect the `Flow` results until last or [Error] value is emitted. Collect + * data using [collector]. + * + * Note, that this function rethrows any [Throwable] exception thrown by [collector] function. + */ +suspend fun Flow>.collectWithLastErrorOrSuccessStatusSimple( + defaultValue: ResultWrapper, + collector: (R, T) -> R +): ResultWrapper { + return collectWithLastErrorOrSuccessStatus( + defaultValue, + { it.succeeded }) + { mergedValue, value -> + value.map { + val mergedData = (mergedValue as ResultWrapper.Success).data + collector(mergedData, it) + } + } +} + +/** + * Collect the `Flow` results until last or not succeeded value is emitted. Collect + * data using [collector]. + * + * Note, that this function rethrows any [Throwable] exception thrown by [collector] function. + */ +suspend fun Flow.collectWithLastErrorOrSuccessStatus( + defaultValue: R, + succeededTest: (T) -> Boolean, + collector: suspend (R, T) -> R +): R { + return transformWhile { + emit(it) + succeededTest(it) + } + .fold(defaultValue, collector) +} diff --git a/app/src/proprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt new file mode 100644 index 0000000..0611172 --- /dev/null +++ b/app/src/proprietary/java/com/app/missednotificationsreminder/payment/di/PurchaseRepositoryModule.kt @@ -0,0 +1,25 @@ +package com.app.missednotificationsreminder.payment.di + +import android.content.Context +import com.app.missednotificationsreminder.data.source.ResourceDataSource +import com.app.missednotificationsreminder.di.qualifiers.ForApplication +import com.app.missednotificationsreminder.payment.billing.data.PurchaseRepositoryImpl +import com.app.missednotificationsreminder.payment.billing.domain.repository.PurchaseRepository +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import javax.inject.Singleton + +@Module +class PurchaseRepositoryModule { + + @Provides + @Singleton + fun providePurchaseRepository( + @ForApplication context: Context, + resourceDataSource: ResourceDataSource + ): PurchaseRepository { + return PurchaseRepositoryImpl(CoroutineScope(Dispatchers.Main), resourceDataSource, context) + } +}