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>
277 lines
8.4 KiB
Markdown
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
|