# Android Subscription & Upgrade UI Parity Plan ## Goal Bring Android subscription/upgrade functionality and UX to match iOS implementation: 1. Show full inline paywall (not teaser + dialog) 2. Implement Google Play Billing integration 3. Disable FAB when upgrade screen is showing ## Current State ### iOS (Reference) - `UpgradeFeatureView` shows 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 - `StoreKitManager` fully implemented ### Android (Current) - `UpgradeFeatureScreen` shows simple teaser → opens `UpgradePromptDialog` - FAB always enabled - `BillingManager` is 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 version - `composeApp/build.gradle.kts` - Add dependency to androidMain ```toml # libs.versions.toml billing = "7.1.1" [libraries] google-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" } ``` ```kotlin # build.gradle.kts - androidMain.dependencies implementation(libs.google.billing) ``` --- ### Phase 2: Implement BillingManager **File:** `composeApp/src/androidMain/kotlin/com/example/mycrib/platform/BillingManager.kt` Replace stub implementation with full Google Play Billing: ```kotlin class BillingManager private constructor(private val context: Context) { // Product IDs (match Google Play Console) private val productIDs = listOf( "com.example.mycrib.pro.monthly", "com.example.mycrib.pro.annual" ) // BillingClient instance private var billingClient: BillingClient // StateFlows for UI val products = MutableStateFlow>(emptyList()) val purchasedProductIDs = MutableStateFlow>(emptySet()) val isLoading = MutableStateFlow(false) val purchaseError = MutableStateFlow(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:** 1. Initialize BillingClient with PurchasesUpdatedListener 2. Handle billing connection state (retry on disconnect) 3. Query products using QueryProductDetailsParams with ProductType.SUBS 4. Launch purchase flow with BillingFlowParams 5. Process purchase results and verify with backend 6. Acknowledge purchases (required or they refund after 3 days) 7. Update SubscriptionCache after successful verification --- ### Phase 3: Update UpgradeFeatureScreen **File:** `composeApp/src/commonMain/kotlin/com/example/mycrib/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:** ```kotlin @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 composable - `SubscriptionProductButton` - Display product with name, price, optional savings badge --- ### Phase 4: Create Android-Specific Product Display **File:** `composeApp/src/androidMain/kotlin/com/example/mycrib/ui/subscription/SubscriptionProductButton.kt` ```kotlin @Composable fun SubscriptionProductButton( productDetails: ProductDetails, isSelected: Boolean, isProcessing: Boolean, onSelect: () -> Unit ) { // Display: // - Product name (e.g., "MyCrib Pro Monthly") // - Price from subscriptionOfferDetails // - "Save X%" badge for annual // - Loading indicator when processing } ``` **Helper function for savings calculation:** ```kotlin fun calculateAnnualSavings(monthly: ProductDetails, annual: ProductDetails): Int? ``` --- ### Phase 5: Disable FAB When Upgrade Showing **Files to modify:** - `composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt` - `composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentsScreen.kt` **Changes:** ```kotlin // 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/mycrib/MainActivity.kt` ```kotlin 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:** 1. **UpgradeFeatureScreen** observes BillingManager.products StateFlow 2. User taps product → calls BillingManager.purchase(activity, productDetails) 3. **BillingManager** launches Google Play purchase UI 4. On success → calls SubscriptionApi.verifyAndroidPurchase() 5. Backend verifies with Google → updates user's subscription tier 6. **BillingManager** calls SubscriptionApi.getSubscriptionStatus() 7. Updates **SubscriptionCache** with new status 8. 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 manager - `iosApp/iosApp/Subscription/UpgradeFeatureView.swift` - Inline paywall UI - `iosApp/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.example.mycrib.pro.monthly`, `com.example.mycrib.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