Files
honeyDueKMP/docs/ANDROID_SUBSCRIPTION_PLAN.md
Trey t 7b0a0e5d85 Implement Android subscription system with freemium limitations
Major subscription system implementation for Android:

BillingManager (Android):
- Full Google Play Billing Library integration
- Product loading, purchase flow, and acknowledgment
- Backend verification via APILayer.verifyAndroidPurchase()
- Purchase restoration for returning users
- Error handling and connection state management

SubscriptionHelper (Shared):
- New limit checking methods: isResidencesBlocked(), isTasksBlocked(),
  isContractorsBlocked(), isDocumentsBlocked()
- Add permission checks: canAddProperty(), canAddTask(),
  canAddContractor(), canAddDocument()
- Enforces freemium rules based on backend limitationsEnabled flag

Screen Updates:
- ContractorsScreen: Show upgrade prompt when contractors limit=0
- DocumentsScreen: Show upgrade prompt when documents limit=0
- ResidencesScreen: Show upgrade prompt when properties limit reached
- ResidenceDetailScreen: Show upgrade prompt when tasks limit reached

UpgradeFeatureScreen:
- Enhanced with feature benefits comparison
- Dynamic content from backend upgrade triggers
- Platform-specific purchase buttons

Additional changes:
- DataCache: Added O(1) lookup maps for ID resolution
- New minimal models (TaskMinimal, ContractorMinimal, ResidenceMinimal)
- TaskApi: Added archive/unarchive endpoints
- Added Google Billing Library dependency
- iOS SubscriptionCache and UpgradePromptView updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 11:23:53 -06:00

277 lines
8.4 KiB
Markdown

# 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<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:**
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