Skip to content

Commit 2d48e9c

Browse files
committed
Implement in-app purchases with the slide to unlock
1 parent 5499edf commit 2d48e9c

File tree

7 files changed

+149
-8
lines changed

7 files changed

+149
-8
lines changed

core/data/src/main/kotlin/com/revenuecat/articles/paywall/coredata/repository/PaywallsRepository.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
*/
1616
package com.revenuecat.articles.paywall.coredata.repository
1717

18+
import android.app.Activity
1819
import com.revenuecat.purchases.CustomerInfo
1920
import com.revenuecat.purchases.Offering
21+
import com.revenuecat.purchases.Package
22+
import com.revenuecat.purchases.PurchaseResult
2023
import com.skydoves.sandwich.ApiResponse
2124
import kotlinx.coroutines.flow.Flow
2225

@@ -25,4 +28,9 @@ interface PaywallsRepository {
2528
fun fetchOffering(): Flow<ApiResponse<Offering>>
2629

2730
fun fetchCustomerInfo(): Flow<CustomerInfo?>
31+
32+
fun awaitPurchases(
33+
activity: Activity,
34+
availablePackage: Package,
35+
): Flow<ApiResponse<PurchaseResult>>
2836
}

core/data/src/main/kotlin/com/revenuecat/articles/paywall/coredata/repository/PaywallsRepositoryImpl.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515
*/
1616
package com.revenuecat.articles.paywall.coredata.repository
1717

18+
import android.app.Activity
1819
import com.revenuecat.articles.paywall.core.network.CatArticlesDispatchers
1920
import com.revenuecat.articles.paywall.core.network.Dispatcher
2021
import com.revenuecat.purchases.CustomerInfo
2122
import com.revenuecat.purchases.Offering
23+
import com.revenuecat.purchases.Package
24+
import com.revenuecat.purchases.PurchaseParams
2225
import com.revenuecat.purchases.Purchases
2326
import com.revenuecat.purchases.PurchasesException
2427
import com.revenuecat.purchases.awaitCustomerInfo
2528
import com.revenuecat.purchases.awaitOfferings
29+
import com.revenuecat.purchases.awaitPurchase
2630
import com.skydoves.sandwich.ApiResponse
2731
import kotlinx.coroutines.CoroutineDispatcher
2832
import kotlinx.coroutines.flow.Flow
@@ -54,4 +58,18 @@ internal class PaywallsRepositoryImpl @Inject constructor(
5458
emit(null)
5559
}
5660
}
61+
62+
override fun awaitPurchases(activity: Activity, availablePackage: Package) = flow {
63+
try {
64+
val result = Purchases.sharedInstance.awaitPurchase(
65+
purchaseParams = PurchaseParams.Builder(
66+
activity = activity,
67+
packageToPurchase = availablePackage,
68+
).build(),
69+
)
70+
emit(ApiResponse.of { result })
71+
} catch (e: Exception) {
72+
emit(ApiResponse.exception(e))
73+
}
74+
}
5775
}

core/designsystem/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@
2020
<string name="paywall_description">This article is available to members only. \nUnlock it to access exclusive member benefits.</string>
2121
<string name="paywall_cta">Join Cat Articles</string>
2222
<string name="entitlement_premium">premium</string>
23+
<string name="slide_subscribe">Slide to subscribe</string>
2324
</resources>

feature/paywalls/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ dependencies {
3535
implementation(libs.androidx.compose.foundation)
3636
implementation(libs.androidx.compose.material)
3737
implementation(libs.androidx.compose.runtime)
38+
implementation(libs.compose.slidetounlock)
3839
}

feature/paywalls/src/main/kotlin/com/revenuecat/articles/paywall/paywalls/CatCustomPaywalls.kt

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,29 @@
1515
*/
1616
package com.revenuecat.articles.paywall.paywalls
1717

18+
import android.app.Activity
1819
import android.widget.Toast
1920
import androidx.compose.foundation.background
2021
import androidx.compose.foundation.layout.Box
2122
import androidx.compose.foundation.layout.fillMaxSize
23+
import androidx.compose.foundation.layout.fillMaxWidth
2224
import androidx.compose.foundation.layout.padding
2325
import androidx.compose.runtime.Composable
2426
import androidx.compose.runtime.getValue
2527
import androidx.compose.runtime.mutableStateOf
2628
import androidx.compose.runtime.remember
2729
import androidx.compose.runtime.setValue
30+
import androidx.compose.ui.Alignment
2831
import androidx.compose.ui.Modifier
2932
import androidx.compose.ui.graphics.Color
3033
import androidx.compose.ui.platform.LocalContext
34+
import androidx.compose.ui.res.stringResource
3135
import androidx.compose.ui.unit.dp
3236
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
3337
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3438
import com.revenuecat.purchases.Offering
39+
import com.revenuecat.purchases.slidetounlock.HintTexts
40+
import com.revenuecat.purchases.slidetounlock.SlideToUnlock
3541
import com.revenuecat.purchases.ui.revenuecatui.Paywall
3642
import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
3743
import com.skydoves.compose.effects.RememberedEffect
@@ -41,21 +47,56 @@ public fun CatCustomPaywalls(
4147
viewModel: CustomCatPaywallsViewModel = hiltViewModel(),
4248
) {
4349
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
50+
val purchaseUiState by viewModel.purchaseUiState.collectAsStateWithLifecycle()
51+
4452
var offering: Offering? by remember { mutableStateOf(null) }
53+
var isSlided by remember { mutableStateOf(false) }
4554

4655
HandleUiState(uiState = uiState) { offering = it }
4756

57+
HandlePurchaseUiState(
58+
uiState = purchaseUiState,
59+
onPurchaseSuccess = { viewModel.navigateUp() },
60+
onPurchaseFailed = { isSlided = false },
61+
)
62+
4863
Box(
4964
modifier = Modifier
5065
.fillMaxSize()
51-
.background(Color.White)
52-
.padding(bottom = 100.dp),
66+
.background(Color.White),
5367
) {
5468
Paywall(
5569
options = PaywallOptions.Builder(
5670
dismissRequest = { viewModel.navigateUp() },
5771
).setOffering(offering).build(),
5872
)
73+
74+
val availablePackage = offering?.availablePackages?.first()
75+
val activity = (LocalContext.current as? Activity)
76+
77+
if (availablePackage != null && activity != null) {
78+
SlideToUnlock(
79+
modifier = Modifier
80+
.align(Alignment.BottomCenter)
81+
.fillMaxWidth()
82+
.padding(bottom = 60.dp, start = 20.dp, end = 20.dp),
83+
isSlided = isSlided,
84+
hintTexts = HintTexts.defaultHintTexts().copy(
85+
defaultText = stringResource(
86+
com.revenuecat.articles.paywall.compose.core.designsystem.R.string.slide_subscribe,
87+
),
88+
),
89+
onSlideCompleted = {
90+
isSlided = true
91+
viewModel.handleEvent(
92+
PaywallEvent.Purchases(
93+
activity = activity,
94+
availablePackage = availablePackage,
95+
),
96+
)
97+
},
98+
)
99+
}
59100
}
60101
}
61102

@@ -74,3 +115,24 @@ private fun HandleUiState(
74115
}
75116
}
76117
}
118+
119+
@Composable
120+
private fun HandlePurchaseUiState(
121+
uiState: PurchaseUiState,
122+
viewModel: CustomCatPaywallsViewModel = hiltViewModel(),
123+
onPurchaseSuccess: () -> Unit,
124+
onPurchaseFailed: () -> Unit,
125+
) {
126+
val context = LocalContext.current
127+
128+
RememberedEffect(key1 = uiState) {
129+
if (uiState is PurchaseUiState.Success) {
130+
onPurchaseSuccess.invoke()
131+
} else if (uiState is PurchaseUiState.Error) {
132+
onPurchaseFailed.invoke()
133+
Toast.makeText(context, uiState.message, Toast.LENGTH_SHORT).show()
134+
}
135+
136+
viewModel.handleEvent(PaywallEvent.None)
137+
}
138+
}

feature/paywalls/src/main/kotlin/com/revenuecat/articles/paywall/paywalls/CustomCatPaywallsViewModel.kt

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,24 @@
1515
*/
1616
package com.revenuecat.articles.paywall.paywalls
1717

18+
import android.app.Activity
1819
import androidx.compose.runtime.Stable
1920
import androidx.lifecycle.ViewModel
2021
import androidx.lifecycle.viewModelScope
2122
import com.revenuecat.articles.paywall.core.navigation.AppComposeNavigator
2223
import com.revenuecat.articles.paywall.core.navigation.CatArticlesScreen
2324
import com.revenuecat.articles.paywall.coredata.repository.PaywallsRepository
2425
import com.revenuecat.purchases.Offering
26+
import com.revenuecat.purchases.Package
27+
import com.revenuecat.purchases.PurchaseResult
2528
import com.skydoves.sandwich.fold
2629
import dagger.hilt.android.lifecycle.HiltViewModel
30+
import kotlinx.coroutines.flow.MutableSharedFlow
2731
import kotlinx.coroutines.flow.SharingStarted
2832
import kotlinx.coroutines.flow.StateFlow
33+
import kotlinx.coroutines.flow.flatMapLatest
34+
import kotlinx.coroutines.flow.flowOf
35+
import kotlinx.coroutines.flow.map
2936
import kotlinx.coroutines.flow.mapLatest
3037
import kotlinx.coroutines.flow.stateIn
3138
import javax.inject.Inject
@@ -36,12 +43,6 @@ class CustomCatPaywallsViewModel @Inject constructor(
3643
private val navigator: AppComposeNavigator<CatArticlesScreen>,
3744
) : ViewModel() {
3845

39-
val customerInfo = repository.fetchCustomerInfo().stateIn(
40-
scope = viewModelScope,
41-
started = SharingStarted.WhileSubscribed(5000),
42-
initialValue = null,
43-
)
44-
4546
val uiState: StateFlow<PaywallsUiState> = repository.fetchOffering()
4647
.mapLatest { response ->
4748
response.fold(
@@ -54,11 +55,49 @@ class CustomCatPaywallsViewModel @Inject constructor(
5455
initialValue = PaywallsUiState.Loading,
5556
)
5657

58+
val event: MutableSharedFlow<PaywallEvent> = MutableSharedFlow(replay = 1)
59+
val purchaseUiState: StateFlow<PurchaseUiState> = event.flatMapLatest { event ->
60+
when (event) {
61+
PaywallEvent.None -> flowOf(PurchaseUiState.None)
62+
63+
is PaywallEvent.Purchases -> {
64+
repository.awaitPurchases(
65+
activity = event.activity,
66+
availablePackage = event.availablePackage,
67+
).map { response ->
68+
response.fold(
69+
onSuccess = { PurchaseUiState.Success(it) },
70+
onFailure = { PurchaseUiState.Error(it) },
71+
)
72+
}
73+
}
74+
}
75+
}.stateIn(
76+
scope = viewModelScope,
77+
started = SharingStarted.WhileSubscribed(5000),
78+
initialValue = PurchaseUiState.None,
79+
)
80+
81+
fun handleEvent(event: PaywallEvent) {
82+
this.event.tryEmit(event)
83+
}
84+
5785
fun navigateUp() {
5886
navigator.navigateUp()
5987
}
6088
}
6189

90+
@Stable
91+
sealed interface PaywallEvent {
92+
93+
data object None : PaywallEvent
94+
95+
data class Purchases(
96+
val activity: Activity,
97+
val availablePackage: Package,
98+
) : PaywallEvent
99+
}
100+
62101
@Stable
63102
sealed interface PaywallsUiState {
64103

@@ -68,3 +107,13 @@ sealed interface PaywallsUiState {
68107

69108
data class Error(val message: String?) : PaywallsUiState
70109
}
110+
111+
@Stable
112+
sealed interface PurchaseUiState {
113+
114+
data object None : PurchaseUiState
115+
116+
data class Success(val purchaseResult: PurchaseResult) : PurchaseUiState
117+
118+
data class Error(val message: String?) : PurchaseUiState
119+
}

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ okhttp = "5.1.0"
2424
landscapist = "2.6.0"
2525
retrofit = "3.0.0"
2626
sandwich = "2.1.3"
27+
slidetounlock = "1.0.2"
2728
baselineprofile = "1.4.1"
2829
spotless = "6.21.0"
2930
androidxJunit = "1.2.1"
@@ -56,6 +57,7 @@ androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "
5657
androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" }
5758
compose-stable-marker = { group = "com.github.skydoves", name = "compose-stable-marker", version.ref = "composeStableMarker" }
5859
compose-effects = { module = "com.github.skydoves:compose-effects", version.ref = "composeEffects" }
60+
compose-slidetounlock = { module = "com.revenuecat.purchases:slide-to-unlock", version.ref = "slidetounlock" }
5961
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
6062
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
6163
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }

0 commit comments

Comments
 (0)