Limited-time offer bottom sheet for Jetpack Compose
LimitedOfferSheet is design 5 of M3 Expressive Paywall. A ModalBottomSheet you can show over any screen: a big discount, the original price struck through, a countdown and a wavy progress bar that drains as time runs out. When the time is up, the button switches to “Offer ended” and is disabled.


implementation("com.github.sanikantv:m3-expressive-paywall:v1.0.0")When to use it
Section titled “When to use it”- Win-back offers for lapsed subscribers.
- A launch or seasonal discount with a real deadline.
// LimitedOfferSheet takes a material3 SheetState, which material3 marks experimental.@OptIn(ExperimentalMaterial3Api::class)@Composablefun OfferScreen(buy: (PaywallPlan) -> Unit, restore: () -> Unit) { var showOffer by rememberSaveable { mutableStateOf(true) } // Keep the deadline stable. In a real app, store it (e.g. in DataStore) so it survives restarts. val endsAt = remember { System.currentTimeMillis() + 15 * 60_000L }
if (showOffer) { LimitedOfferSheet( plan = Sample.yearly.copy( price = "$14.99", trialText = null, priceDetail = "First year only, then $29.99 per year", ), originalPrice = "$29.99", discountLabel = "-50%", offerEndsAtMillis = endsAt, offerDurationMillis = 15 * 60_000L, legal = Sample.legal, onPurchase = buy, onRestore = restore, onClose = { showOffer = false }, ) }}Parameters
Section titled “Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
plan |
PaywallPlan |
required | The discounted plan. Its price is the offer price. |
originalPrice |
String |
required | Regular price, shown struck through. |
discountLabel |
String |
required | Short text in the badge, such as “-50%”. |
offerEndsAtMillis |
Long |
required | When the offer ends, as System.currentTimeMillis() time. |
legal |
PaywallLegal |
required | Terms and Privacy URLs for the footer links. |
onPurchase |
(PaywallPlan) -> Unit |
required | Called with the selected plan when the button is tapped. Start your billing flow here. |
onRestore |
() -> Unit |
required | Called from the Restore link. |
onClose |
() -> Unit |
required | Called from the close button. |
modifier |
Modifier |
Modifier |
Applied to the sheet. |
title |
String |
“Limited-time offer” | Headline. |
subtitle |
String |
“A special price, just for you.” | Line under the headline. |
offerDurationMillis |
Long |
15 * 60_000L |
Full length of the offer. Scales the progress bar. |
sheetState |
SheetState |
Hidden, skips the half-expanded state | Sheet state, if you need to control it. |
Full signature: LimitedOfferSheet in the API reference.
Customization
Section titled “Customization”Intro prices need the full story: put “First year only, then $29.99 per year” in priceDetail. onClose is called both by the close button and by swiping the sheet down.
Accessibility
Section titled “Accessibility”- The countdown is announced as one phrase (“Offer ends in 12 min 30 sec”) instead of separate digits.
- The sheet has a close button in addition to swipe-to-dismiss.
Common mistakes
Section titled “Common mistakes”- Resetting the deadline on every launch. Fake urgency breaks consumer protection rules; store the real end time.
- Leaving out the regular price after the intro period.
Related
Section titled “Related”Built by Sanikant. Set in Google Sans Flex (SIL Open Font License). Not affiliated with Google.