Total rebrand across KMM project: - Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations) - Gradle: rootProject.name, namespace, applicationId - Android: manifest, strings.xml (all languages), widget resources - iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig - iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc. - Swift source: all class/struct/enum renames - Deep links: casera:// -> honeydue://, .casera -> .honeydue - App icons replaced with honeyDue honeycomb icon - Domains: casera.treytartt.com -> honeyDue.treytartt.com - Bundle IDs: com.tt.casera -> com.tt.honeyDue - Database table names preserved Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.4 KiB
Android Subscription & Upgrade UI Parity Plan
Goal
Bring Android subscription/upgrade functionality and UX to match iOS implementation:
- Show full inline paywall (not teaser + dialog)
- Implement Google Play Billing integration
- Disable FAB when upgrade screen is showing
Current State
iOS (Reference)
UpgradeFeatureViewshows full inline paywall with:- Promo content card with feature bullets
- Subscription product buttons with real pricing
- Purchase flow via StoreKit 2
- "Compare Free vs Pro" and "Restore Purchases" links
- Add button disabled/grayed when upgrade showing
StoreKitManagerfully implemented
Android (Current)
UpgradeFeatureScreenshows simple teaser → opensUpgradePromptDialog- FAB always enabled
BillingManageris a stub (no real billing)- No Google Play Billing dependency
Implementation Plan
Phase 1: Add Google Play Billing Dependency
Files to modify:
gradle/libs.versions.toml- Add billing library versioncomposeApp/build.gradle.kts- Add dependency to androidMain
# libs.versions.toml
billing = "7.1.1"
[libraries]
google-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" }
# build.gradle.kts - androidMain.dependencies
implementation(libs.google.billing)
Phase 2: Implement BillingManager
File: composeApp/src/androidMain/kotlin/com/example/honeydue/platform/BillingManager.kt
Replace stub implementation with full Google Play Billing:
class BillingManager private constructor(private val context: Context) {
// Product IDs (match Google Play Console)
private val productIDs = listOf(
"com.tt.honeyDue.pro.monthly",
"com.tt.honeyDue.pro.annual"
)
// BillingClient instance
private var billingClient: BillingClient
// StateFlows for UI
val products = MutableStateFlow<List<ProductDetails>>(emptyList())
val purchasedProductIDs = MutableStateFlow<Set<String>>(emptySet())
val isLoading = MutableStateFlow(false)
val purchaseError = MutableStateFlow<String?>(null)
// Key methods to implement:
- startConnection() - Connect to Google Play
- loadProducts() - Query subscription products
- purchase(activity, productDetails) - Launch purchase flow
- restorePurchases() - Query purchase history
- verifyPurchaseWithBackend() - Call SubscriptionApi.verifyAndroidPurchase()
- acknowledgePurchase() - Required by Google
- listenForPurchases() - PurchasesUpdatedListener
Key implementation details:
- Initialize BillingClient with PurchasesUpdatedListener
- Handle billing connection state (retry on disconnect)
- Query products using QueryProductDetailsParams with ProductType.SUBS
- Launch purchase flow with BillingFlowParams
- Process purchase results and verify with backend
- Acknowledge purchases (required or they refund after 3 days)
- Update SubscriptionCache after successful verification
Phase 3: Update UpgradeFeatureScreen
File: composeApp/src/commonMain/kotlin/com/example/honeydue/ui/subscription/UpgradeFeatureScreen.kt
Transform from teaser+dialog to full inline paywall matching iOS:
Current structure:
- Icon, title, message, badge
- Button opens UpgradePromptDialog
New structure:
@Composable
fun UpgradeFeatureScreen(
triggerKey: String,
icon: ImageVector,
onNavigateBack: () -> Unit,
billingManager: BillingManager? = null // Android-only, null on other platforms
) {
// ScrollView with:
// 1. Star icon (accent gradient)
// 2. Title + message from triggerData
// 3. PromoContentCard - feature bullets from triggerData.promoHtml
// 4. SubscriptionProductButtons - show real products with pricing
// 5. "Compare Free vs Pro" button
// 6. "Restore Purchases" button
// 7. Error display if any
}
New components to add:
PromoContentCard- Parse and display promo HTML as composableSubscriptionProductButton- Display product with name, price, optional savings badge
Phase 4: Create Android-Specific Product Display
File: composeApp/src/androidMain/kotlin/com/example/honeydue/ui/subscription/SubscriptionProductButton.kt
@Composable
fun SubscriptionProductButton(
productDetails: ProductDetails,
isSelected: Boolean,
isProcessing: Boolean,
onSelect: () -> Unit
) {
// Display:
// - Product name (e.g., "HoneyDue Pro Monthly")
// - Price from subscriptionOfferDetails
// - "Save X%" badge for annual
// - Loading indicator when processing
}
Helper function for savings calculation:
fun calculateAnnualSavings(monthly: ProductDetails, annual: ProductDetails): Int?
Phase 5: Disable FAB When Upgrade Showing
Files to modify:
composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/ContractorsScreen.ktcomposeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/DocumentsScreen.kt
Changes:
// In ContractorsScreen
val shouldShowUpgradePrompt = SubscriptionHelper.shouldShowUpgradePromptForContractors().allowed
// Update FAB
FloatingActionButton(
onClick = { if (!shouldShowUpgradePrompt) showAddDialog = true },
containerColor = if (shouldShowUpgradePrompt)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
else
MaterialTheme.colorScheme.primary,
contentColor = if (shouldShowUpgradePrompt)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.onPrimary
) {
Icon(Icons.Default.Add, "Add contractor")
}
// Add .alpha() modifier or enabled state
Same pattern for DocumentsScreen.
Phase 6: Initialize BillingManager in MainActivity
File: composeApp/src/androidMain/kotlin/com/example/honeydue/MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Existing initializations...
TokenStorage.initialize(...)
ThemeStorage.initialize(...)
ThemeManager.initialize()
// Add BillingManager initialization
val billingManager = BillingManager.getInstance(applicationContext)
billingManager.startConnection(
onSuccess = { billingManager.loadProducts() },
onError = { error -> Log.e("Billing", "Connection failed: $error") }
)
setContent {
// Pass billingManager to composables that need it
App(billingManager = billingManager)
}
}
Phase 7: Wire Purchase Flow End-to-End
Integration points:
- UpgradeFeatureScreen observes BillingManager.products StateFlow
- User taps product → calls BillingManager.purchase(activity, productDetails)
- BillingManager launches Google Play purchase UI
- On success → calls SubscriptionApi.verifyAndroidPurchase()
- Backend verifies with Google → updates user's subscription tier
- BillingManager calls SubscriptionApi.getSubscriptionStatus()
- Updates SubscriptionCache with new status
- UI recomposes, upgrade screen disappears, FAB becomes enabled
Files Summary
| File | Action |
|---|---|
gradle/libs.versions.toml |
Add billing version |
composeApp/build.gradle.kts |
Add billing dependency |
BillingManager.kt |
Full rewrite with real billing |
UpgradeFeatureScreen.kt |
Transform to inline paywall |
ContractorsScreen.kt |
Disable FAB when upgrade showing |
DocumentsScreen.kt |
Disable FAB when upgrade showing |
MainActivity.kt |
Initialize BillingManager |
Reference Files (iOS Implementation)
These files show the iOS implementation to mirror:
iosApp/iosApp/Subscription/StoreKitManager.swift- Full billing manageriosApp/iosApp/Subscription/UpgradeFeatureView.swift- Inline paywall UIiosApp/iosApp/Subscription/UpgradePromptView.swift- PromoContentView, SubscriptionProductButton
Testing Checklist
- Products load from Google Play Console
- Purchase flow launches correctly
- Purchase verification with backend works
- SubscriptionCache updates after purchase
- FAB disabled when upgrade prompt showing
- FAB enabled after successful purchase
- Restore purchases works
- Error states display correctly
Notes
- Product IDs must match Google Play Console:
com.tt.honeyDue.pro.monthly,com.tt.honeyDue.pro.annual - Backend endpoint
POST /subscription/verify-android/already exists in SubscriptionApi - Testing requires Google Play Console setup with test products
- Use Google's test cards for sandbox testing