HorizonBillingSample directory in Android Studio. The project includes two build variants: questDebug/questRelease for Meta Quest and mobileDebug/mobileRelease for Android mobile. Select the appropriate variant from the Build Variants panel, then build and run on your target device. For detailed build instructions and configuration requirements, see the sample’s README.| File / Directory | What it demonstrates | Key concepts |
|---|---|---|
app/src/main/ | Shared code: activities, billing interface, data models, utilities | Core abstractions that work across platforms |
purchase/IBillingHandler.kt | Platform-agnostic billing interface with 6 methods | Strategy pattern for swappable implementations |
purchase/CoreProductType.kt | Platform-independent product type enum ( INAPP, SUBS) | Abstraction over platform-specific billing types |
activities/MainActivity.kt | Main UI with 6 purchase buttons and view purchase history | Billing handler injection via Hilt |
activities/ViewPurchasesActivity.kt | Purchase history screen with Consume button for consumables | Querying and consuming purchases |
activities/PurchaseCompleteActivity.kt | Success screen showing order details | Purchase acknowledgment flow |
activities/PurchaseErrorActivity.kt | Error handling with mapped response codes | BillingResponseCode interpretation |
app/src/quest/QuestAppPurchaseHandler.kt | Quest-specific implementation using Horizon Billing SDK | setAppId() builder configuration |
app/src/mobile/MobileAppPurchaseHandler.kt | Mobile-specific implementation using Google Play Billing | enablePendingPurchases() builder configuration |
HorizonBillingSampleApplication.kt | Hilt application class | Dependency injection setup |
IBillingHandler with six methods that cover the complete purchase lifecycle: initialize(), requestProductDetails(), requestPurchases(), launchBillingFlow(), acknowledgePurchase(), and consumePurchase(). Each platform implements this interface using its respective billing SDK. The interface returns List<Any> from requestProductDetails() because the ProductDetails class differs between platforms — each handler casts internally when launching the billing flow. See the complete interface in purchase/IBillingHandler.kt.devices flavor dimension with mobile and quest flavors. Each flavor has its own source set (app/src/quest/ and app/src/mobile/) containing a Hilt module that provides IBillingHandler. Because each module lives in its respective source set, only one is compiled into any given build:@Module
@InstallIn(SingletonComponent::class)
class QuestAppPurchaseModule {
@Singleton @Provides
fun provideAppPurchaseHandler(): IBillingHandler = QuestAppPurchaseHandler()
}
@Inject lateinit var billingHandler: IBillingHandler. Hilt automatically provides the correct implementation based on the build variant. See quest/QuestAppPurchaseHandler.kt and mobile/MobileAppPurchaseHandler.kt for the complete module definitions.setAppId(BuildConfig.QUEST_APP_ID) while mobile uses enablePendingPurchases(PendingPurchasesParams). This design minimizes platform-specific code. Both handlers cover all 12 BillingResponseCode values: OK, ERROR, BILLING_UNAVAILABLE, DEVELOPER_ERROR, FEATURE_NOT_SUPPORTED, ITEM_ALREADY_OWNED, ITEM_NOT_OWNED, ITEM_UNAVAILABLE, NETWORK_ERROR, SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, USER_CANCELED, plus a default branch.ViewPurchasesActivity displays a Consume button that calls billingHandler.consumePurchase(). The sample determines consumability by checking if the product name contains “consume” — a heuristic suitable for demonstration purposes. See the acknowledgment logic in activities/MainActivity.kt and the consumption flow in activities/ViewPurchasesActivity.kt.SUBS purchases separately and displays subscription details, expiration dates, and renewal status