IMPORTANT: The file content has been truncated. Status: Showing lines 57-128 of 372 total lines. Action: To read more of the file, you can use the 'start_line' and 'end_line' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use start_line: 129. --- FILE CONTENT (truncated) --- private val _hasPendingPurchase = MutableStateFlow(false) override val hasPendingPurchase: StateFlow = _hasPendingPurchase.asStateFlow() /** * Holds the premium status as determined by the most recent successful billing query, * or `null` if no billing query has completed yet. When `null`, [isPremium] falls back * to the DataStore-cached value so there is no flicker before the billing client connects. */ private val billingDerivedPremium = MutableStateFlow(null) /** * Mirrors the [com.servicebook.data.preferences.AppPreferences.isPremiumCached] field from * DataStore, collected once via [stateIn] so that [processPurchases] can read the latest * value synchronously (via `.value`) without an extra [kotlinx.coroutines.flow.Flow.first] * suspension on every billing update. * * Seeded with `null` (not `false`) to signal "not yet loaded" — until the first DataStore * emission arrives, [isPremium] falls back to `false` via the combine's `?: false` default * rather than treating the absence of data as an explicit non-premium signal. */ private val cachedPremiumFlow: StateFlow = preferences.preferences .map { it.isPremiumCached } .stateIn(coroutineScope, SharingStarted.Eagerly, null) private val cachedProductDetails = AtomicReference(null) private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { coroutineScope.launch { processPurchases(purchases.orEmpty()) } } BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> { // Play may return ITEM_ALREADY_OWNED without a purchase list; re-query to // ensure the existing entitlement is reflected in isPremium and the cache. coroutineScope.launch { queryPurchases() } } // USER_CANCELED and other non-success responses are handled gracefully by doing nothing, // but we clear the pending state as the purchase flow is no longer active. else -> { _hasPendingPurchase.value = false } } } private val billingClient: BillingClient = billingClientProvider.provide(purchasesUpdatedListener) init { // Derive isPremium by combining the billing-query result, the DataStore cache, // and any active debug overrides. coroutineScope.launch { if (debugOverride != null) { combine( billingDerivedPremium, cachedPremiumFlow, debugOverride.mode, ) { billingPremium, cachedPremium, overrideMode -> when (overrideMode) { DebugBillingOverrideMode.FORCE_FREE -> false DebugBillingOverrideMode.FORCE_PREMIUM -> true DebugBillingOverrideMode.NONE -> billingPremium ?: cachedPremium ?: false } }.collect { _isPremium.value = it } } else { combine( billingDerivedPremium, cachedPremiumFlow, ) { billingPremium, cachedPremium -> billingPremium ?: cachedPremium ?: false