Skip to content

Commit 59384ef

Browse files
kherembourgclaude
andauthored
feat: embedded paywall, CLIENT type handling, improved images (#6)
* feat(ios): add CLIENT presentation type handling in all placements Guard against .client type presentations across all fetchPresentation call sites (filters, recipe_detail, favorites, onboarding) — log and return instead of attempting to display a server-built screen the app must render itself. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(android): add embedded paywall banner and CLIENT type handling - Add EmbeddedPaywallBanner composable using Purchasely.presentationView() with placement "home_banner", shown as a LazyVerticalGrid header item for non-premium users on the Home screen - Add PLYPresentationType.CLIENT guard in all fetchPresentation call sites (HomeScreen/filters, DetailScreen/favorites+recipe_detail, FavoritesScreen/favorites, SettingsScreen/onboarding) — logs and skips display() for client-built paywalls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d9b9f53 commit 59384ef

9 files changed

Lines changed: 163 additions & 41 deletions

File tree

android/app/src/main/java/com/purchasely/shaker/ui/screen/detail/DetailScreen.kt

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,22 @@ fun DetailScreen(
8484
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
8585
Purchasely.fetchPresentation("favorites") { presentation, error ->
8686
if (presentation != null && presentation.type != PLYPresentationType.DEACTIVATED) {
87-
presentation.display(activity) { result, plan ->
88-
when (result) {
89-
PLYProductViewResult.PURCHASED,
90-
PLYProductViewResult.RESTORED -> {
91-
Log.d("DetailScreen", "[Shaker] Purchased/Restored from favorites: ${plan?.name}")
92-
viewModel.onPaywallDismissed()
87+
if (presentation.type == PLYPresentationType.CLIENT) {
88+
// PURCHASELY: CLIENT type — app builds its own paywall UI
89+
// The presentation contains plan data but no server-built screen
90+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
91+
Log.d("DetailScreen", "[Shaker] CLIENT presentation received for favorites placement — build custom UI here")
92+
// In a real app, extract plans from presentation and build native UI
93+
} else {
94+
presentation.display(activity) { result, plan ->
95+
when (result) {
96+
PLYProductViewResult.PURCHASED,
97+
PLYProductViewResult.RESTORED -> {
98+
Log.d("DetailScreen", "[Shaker] Purchased/Restored from favorites: ${plan?.name}")
99+
viewModel.onPaywallDismissed()
100+
}
101+
else -> {}
93102
}
94-
else -> {}
95103
}
96104
}
97105
}
@@ -237,20 +245,28 @@ fun DetailScreen(
237245
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
238246
Purchasely.fetchPresentation(properties = PLYPresentationProperties(placementId = "recipe_detail", contentId = c.id)) { presentation, error ->
239247
if (presentation != null && presentation.type != PLYPresentationType.DEACTIVATED) {
240-
presentation.display(activity) { result, plan ->
241-
when (result) {
242-
PLYProductViewResult.PURCHASED -> {
243-
Log.d("DetailScreen", "[Shaker] Purchased: ${plan?.name}")
244-
viewModel.onPaywallDismissed()
245-
}
246-
PLYProductViewResult.RESTORED -> {
247-
Log.d("DetailScreen", "[Shaker] Restored: ${plan?.name}")
248-
viewModel.onPaywallDismissed()
249-
}
250-
PLYProductViewResult.CANCELLED -> {
251-
Log.d("DetailScreen", "[Shaker] Cancelled")
248+
if (presentation.type == PLYPresentationType.CLIENT) {
249+
// PURCHASELY: CLIENT type — app builds its own paywall UI
250+
// The presentation contains plan data but no server-built screen
251+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
252+
Log.d("DetailScreen", "[Shaker] CLIENT presentation received for recipe_detail placement — build custom UI here")
253+
// In a real app, extract plans from presentation and build native UI
254+
} else {
255+
presentation.display(activity) { result, plan ->
256+
when (result) {
257+
PLYProductViewResult.PURCHASED -> {
258+
Log.d("DetailScreen", "[Shaker] Purchased: ${plan?.name}")
259+
viewModel.onPaywallDismissed()
260+
}
261+
PLYProductViewResult.RESTORED -> {
262+
Log.d("DetailScreen", "[Shaker] Restored: ${plan?.name}")
263+
viewModel.onPaywallDismissed()
264+
}
265+
PLYProductViewResult.CANCELLED -> {
266+
Log.d("DetailScreen", "[Shaker] Cancelled")
267+
}
268+
else -> {}
252269
}
253-
else -> {}
254270
}
255271
}
256272
}

android/app/src/main/java/com/purchasely/shaker/ui/screen/favorites/FavoritesScreen.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,22 @@ fun FavoritesScreen(
8888
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
8989
Purchasely.fetchPresentation("favorites") { presentation, error ->
9090
if (presentation != null && presentation.type != PLYPresentationType.DEACTIVATED) {
91-
presentation.display(activity) { result, plan ->
92-
when (result) {
93-
PLYProductViewResult.PURCHASED,
94-
PLYProductViewResult.RESTORED -> {
95-
Log.d("FavoritesScreen", "[Shaker] Purchased/Restored: ${plan?.name}")
96-
viewModel.onPaywallDismissed()
91+
if (presentation.type == PLYPresentationType.CLIENT) {
92+
// PURCHASELY: CLIENT type — app builds its own paywall UI
93+
// The presentation contains plan data but no server-built screen
94+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
95+
Log.d("FavoritesScreen", "[Shaker] CLIENT presentation received for favorites placement — build custom UI here")
96+
// In a real app, extract plans from presentation and build native UI
97+
} else {
98+
presentation.display(activity) { result, plan ->
99+
when (result) {
100+
PLYProductViewResult.PURCHASED,
101+
PLYProductViewResult.RESTORED -> {
102+
Log.d("FavoritesScreen", "[Shaker] Purchased/Restored: ${plan?.name}")
103+
viewModel.onPaywallDismissed()
104+
}
105+
else -> {}
97106
}
98-
else -> {}
99107
}
100108
}
101109
}

android/app/src/main/java/com/purchasely/shaker/ui/screen/home/HomeScreen.kt

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.aspectRatio
1313
import androidx.compose.foundation.layout.fillMaxSize
1414
import androidx.compose.foundation.layout.fillMaxWidth
1515
import androidx.compose.foundation.layout.height
16+
import androidx.compose.foundation.layout.heightIn
1617
import androidx.compose.foundation.layout.padding
1718
import androidx.compose.foundation.layout.size
1819
import androidx.compose.foundation.lazy.grid.GridCells
20+
import androidx.compose.foundation.lazy.grid.GridItemSpan
1921
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
2022
import androidx.compose.foundation.lazy.grid.items
2123
import androidx.compose.material.icons.Icons
@@ -43,6 +45,7 @@ import androidx.compose.ui.draw.clip
4345
import androidx.compose.ui.platform.LocalContext
4446
import androidx.compose.ui.text.style.TextOverflow
4547
import androidx.compose.ui.unit.dp
48+
import androidx.compose.ui.viewinterop.AndroidView
4649
import com.purchasely.shaker.domain.model.Cocktail
4750
import com.purchasely.shaker.ui.components.CocktailImage
4851
import io.purchasely.ext.PLYPresentationType
@@ -85,14 +88,22 @@ fun HomeScreen(
8588
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
8689
Purchasely.fetchPresentation("filters") { presentation, error ->
8790
if (presentation != null && presentation.type != PLYPresentationType.DEACTIVATED) {
88-
presentation.display(activity) { result, plan ->
89-
when (result) {
90-
PLYProductViewResult.PURCHASED,
91-
PLYProductViewResult.RESTORED -> {
92-
Log.d("HomeScreen", "[Shaker] Purchased/Restored from filters: ${plan?.name}")
93-
viewModel.onPaywallDismissed()
91+
if (presentation.type == PLYPresentationType.CLIENT) {
92+
// PURCHASELY: CLIENT type — app builds its own paywall UI
93+
// The presentation contains plan data but no server-built screen
94+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
95+
Log.d("HomeScreen", "[Shaker] CLIENT presentation received for filters placement — build custom UI here")
96+
// In a real app, extract plans from presentation and build native UI
97+
} else {
98+
presentation.display(activity) { result, plan ->
99+
when (result) {
100+
PLYProductViewResult.PURCHASED,
101+
PLYProductViewResult.RESTORED -> {
102+
Log.d("HomeScreen", "[Shaker] Purchased/Restored from filters: ${plan?.name}")
103+
viewModel.onPaywallDismissed()
104+
}
105+
else -> {}
94106
}
95-
else -> {}
96107
}
97108
}
98109
}
@@ -151,6 +162,14 @@ fun HomeScreen(
151162
verticalArrangement = Arrangement.spacedBy(12.dp),
152163
modifier = Modifier.fillMaxSize()
153164
) {
165+
if (!isPremium) {
166+
item(span = { GridItemSpan(2) }) {
167+
// PURCHASELY: Embedded paywall view — displays a paywall inline within the Home screen
168+
// Uses a dedicated placement configured in the Purchasely console
169+
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
170+
EmbeddedPaywallBanner()
171+
}
172+
}
154173
items(cocktails, key = { it.id }) { cocktail ->
155174
CocktailCard(cocktail = cocktail, onClick = { onCocktailClick(cocktail.id) })
156175
}
@@ -166,6 +185,23 @@ fun HomeScreen(
166185
}
167186
}
168187

188+
@Composable
189+
private fun EmbeddedPaywallBanner() {
190+
val context = LocalContext.current
191+
AndroidView(
192+
factory = {
193+
// PURCHASELY: Embedded paywall view — displays a paywall inline within a screen
194+
// Uses a dedicated placement configured in the Purchasely console
195+
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
196+
Purchasely.presentationView(context, "home_banner")
197+
},
198+
modifier = Modifier
199+
.fillMaxWidth()
200+
.heightIn(max = 200.dp)
201+
.padding(horizontal = 0.dp, vertical = 8.dp)
202+
)
203+
}
204+
169205
@Composable
170206
private fun CocktailCard(cocktail: Cocktail, onClick: () -> Unit) {
171207
Card(

android/app/src/main/java/com/purchasely/shaker/ui/screen/settings/SettingsScreen.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,22 @@ fun SettingsScreen(
194194
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
195195
Purchasely.fetchPresentation("onboarding") { presentation, error ->
196196
if (presentation != null && presentation.type != PLYPresentationType.DEACTIVATED) {
197-
presentation.display(activity) { result, plan ->
198-
when (result) {
199-
PLYProductViewResult.PURCHASED,
200-
PLYProductViewResult.RESTORED -> {
201-
Log.d("Settings", "[Shaker] Purchased/Restored from onboarding: ${plan?.name}")
202-
viewModel.onPurchaseCompleted()
197+
if (presentation.type == PLYPresentationType.CLIENT) {
198+
// PURCHASELY: CLIENT type — app builds its own paywall UI
199+
// The presentation contains plan data but no server-built screen
200+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
201+
Log.d("Settings", "[Shaker] CLIENT presentation received for onboarding placement — build custom UI here")
202+
// In a real app, extract plans from presentation and build native UI
203+
} else {
204+
presentation.display(activity) { result, plan ->
205+
when (result) {
206+
PLYProductViewResult.PURCHASED,
207+
PLYProductViewResult.RESTORED -> {
208+
Log.d("Settings", "[Shaker] Purchased/Restored from onboarding: ${plan?.name}")
209+
viewModel.onPurchaseCompleted()
210+
}
211+
else -> {}
203212
}
204-
else -> {}
205213
}
206214
}
207215
} else {

ios/Shaker/Screens/Detail/DetailViewModel.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ class DetailViewModel: ObservableObject {
4848
print("[Shaker] Recipe detail presentation not available: \(error?.localizedDescription ?? "deactivated")")
4949
return
5050
}
51+
52+
if presentation.type == .client {
53+
// PURCHASELY: CLIENT type — app builds its own paywall UI
54+
// The presentation contains plan data but no server-built screen
55+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
56+
print("[Shaker] CLIENT presentation received — build custom UI here")
57+
return
58+
}
59+
5160
DispatchQueue.main.async {
5261
presentation.display(from: vc)
5362
}
@@ -81,6 +90,15 @@ class DetailViewModel: ObservableObject {
8190
print("[Shaker] Favorites presentation not available: \(error?.localizedDescription ?? "deactivated")")
8291
return
8392
}
93+
94+
if presentation.type == .client {
95+
// PURCHASELY: CLIENT type — app builds its own paywall UI
96+
// The presentation contains plan data but no server-built screen
97+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
98+
print("[Shaker] CLIENT presentation received — build custom UI here")
99+
return
100+
}
101+
84102
DispatchQueue.main.async {
85103
presentation.display(from: vc)
86104
}

ios/Shaker/Screens/Favorites/FavoritesScreen.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ struct FavoritesScreen: View {
8282
print("[Shaker] Favorites presentation not available: \(error?.localizedDescription ?? "deactivated")")
8383
return
8484
}
85+
86+
if presentation.type == .client {
87+
// PURCHASELY: CLIENT type — app builds its own paywall UI
88+
// The presentation contains plan data but no server-built screen
89+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
90+
print("[Shaker] CLIENT presentation received — build custom UI here")
91+
return
92+
}
93+
8594
DispatchQueue.main.async {
8695
presentation.display(from: vc)
8796
}

ios/Shaker/Screens/Home/HomeScreen.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ struct HomeScreen: View {
8787
print("[Shaker] Filters presentation not available: \(error?.localizedDescription ?? "deactivated")")
8888
return
8989
}
90+
91+
if presentation.type == .client {
92+
// PURCHASELY: CLIENT type — app builds its own paywall UI
93+
// The presentation contains plan data but no server-built screen
94+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
95+
print("[Shaker] CLIENT presentation received — build custom UI here")
96+
return
97+
}
98+
9099
DispatchQueue.main.async {
91100
presentation.display(from: vc)
92101
}

ios/Shaker/Screens/Onboarding/OnboardingScreen.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ struct OnboardingScreen: View {
3838
return
3939
}
4040

41+
if presentation.type == .client {
42+
// PURCHASELY: CLIENT type — app builds its own paywall UI
43+
// The presentation contains plan data but no server-built screen
44+
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
45+
print("[Shaker] CLIENT presentation received — build custom UI here")
46+
DispatchQueue.main.async { onComplete() }
47+
return
48+
}
49+
4150
DispatchQueue.main.async {
4251
presentation.display(from: hostViewController)
4352
}

0 commit comments

Comments
 (0)