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>
This commit is contained in:
Trey t
2025-11-25 11:23:53 -06:00
parent f9e522f734
commit 7b0a0e5d85
21 changed files with 2316 additions and 549 deletions

View File

@@ -155,6 +155,34 @@ data class MyResidencesResponse(
val residences: List<ResidenceWithTasks>
)
/**
* Minimal residence model for list views.
* Uses property_type_id and annotated counts instead of nested objects.
* Resolve property type via DataCache.getResidenceType(residence.propertyTypeId)
*/
@Serializable
data class ResidenceMinimal(
val id: Int,
val name: String,
@SerialName("property_type_id") val propertyTypeId: Int? = null,
val bedrooms: Int? = null,
val bathrooms: Float? = null,
@SerialName("is_primary") val isPrimary: Boolean = false,
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
@SerialName("user_count") val userCount: Int = 1,
// Annotated counts from database (no N+1 queries)
@SerialName("task_count") val taskCount: Int = 0,
@SerialName("tasks_pending") val tasksPending: Int = 0,
@SerialName("tasks_overdue") val tasksOverdue: Int = 0,
@SerialName("tasks_due_week") val tasksDueWeek: Int = 0,
// Reference to last/next task (just ID and date, not full object)
@SerialName("last_completed_task_id") val lastCompletedTaskId: Int? = null,
@SerialName("last_completed_date") val lastCompletedDate: String? = null,
@SerialName("next_task_id") val nextTaskId: Int? = null,
@SerialName("next_task_date") val nextTaskDate: String? = null,
@SerialName("created_at") val createdAt: String
)
// Share Code Models
@Serializable
data class ResidenceShareCode(