@@ -79,10 +79,10 @@ class OpenIapModule(
7979 android.content.pm.PackageManager .GET_META_DATA
8080 )
8181 val id = appInfo.metaData?.getString(" com.oculus.vr.APP_ID" )
82- android.util. Log .i( TAG , " Read Oculus App ID from manifest: $id " )
82+ OpenIapLog .d( " Read Oculus App ID from manifest: $id " , TAG )
8383 id
8484 } catch (e: Exception ) {
85- android.util. Log .w( TAG , " Failed to read com.oculus.vr.APP_ID from AndroidManifest.xml: ${e.message} " )
85+ OpenIapLog .w( " Failed to read com.oculus.vr.APP_ID from AndroidManifest.xml: ${e.message} " , TAG )
8686 null
8787 }
8888 }
@@ -98,8 +98,9 @@ class OpenIapModule(
9898 private val purchaseErrorListeners = mutableSetOf<OpenIapPurchaseErrorListener >()
9999
100100 init {
101- android.util.Log .i(TAG , " === OpenIapModule INIT (Horizon flavor) ===" )
102- buildBillingClient()
101+ // DO NOT build BillingClient here - React Native context doesn't have Activity yet
102+ // BillingClient will be built in initConnection() when Activity is guaranteed to be available
103+ OpenIapLog .d(" OpenIapModule initialized (Horizon flavor)" , TAG )
103104 }
104105
105106 override fun setActivity (activity : Activity ? ) {
@@ -109,37 +110,35 @@ class OpenIapModule(
109110 override val initConnection: MutationInitConnectionHandler = {
110111 withContext(Dispatchers .IO ) {
111112 suspendCancellableCoroutine<Boolean > { continuation ->
112- android.util. Log .i( TAG , " === INIT CONNECTION CALLED ===" )
113+ OpenIapLog .i( " === INIT CONNECTION ===" , TAG )
113114
114115 // CRITICAL FIX: Rebuild BillingClient if it was destroyed by endConnection
116+ // Use current Activity if available, otherwise fallback to Context
115117 if (billingClient == null ) {
116- android.util.Log .i(TAG , " BillingClient is null, rebuilding..." )
117- buildBillingClient()
118- } else {
119- android.util.Log .i(TAG , " BillingClient already exists, using existing instance" )
118+ val contextForInit = currentActivityRef?.get() ? : fallbackActivity ? : context
119+ OpenIapLog .d(" Building BillingClient with ${contextForInit.javaClass.simpleName} ..." , TAG )
120+ buildBillingClient(contextForInit)
120121 }
121122
122123 val client = billingClient ? : run {
123- android.util. Log .w( TAG , " Failed to build BillingClient" )
124+ OpenIapLog .w( " Failed to build BillingClient" , TAG )
124125 if (continuation.isActive) continuation.resume(false )
125126 return @suspendCancellableCoroutine
126127 }
127128
128- android.util.Log .i(TAG , " Starting BillingClient connection..." )
129129 client.startConnection(object : BillingClientStateListener {
130130 override fun onBillingSetupFinished (result : BillingResult ) {
131- android.util.Log .i(TAG , " onBillingSetupFinished: code=${result.responseCode} , message=${result.debugMessage} " )
132131 val ok = result.responseCode == BillingClient .BillingResponseCode .OK
133132 if (! ok) {
134- android.util. Log .w( TAG , " Horizon setup failed: code=${result.responseCode} , ${result.debugMessage} " )
133+ OpenIapLog .w( " Horizon setup failed: code=${result.responseCode} , ${result.debugMessage} " , TAG )
135134 } else {
136- android.util. Log .i( TAG , " Horizon billing connected successfully! " )
135+ OpenIapLog .i( " Horizon billing connected successfully" , TAG )
137136 }
138137 if (continuation.isActive) continuation.resume(ok)
139138 }
140139
141140 override fun onBillingServiceDisconnected () {
142- android.util. Log .i( TAG , " Horizon service disconnected" )
141+ OpenIapLog .i( " Horizon service disconnected" , TAG )
143142 }
144143 })
145144 }
@@ -212,82 +211,63 @@ class OpenIapModule(
212211 }
213212
214213 override val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { _ ->
215- android.util.Log .i(" HORIZON_QUERY" , " getAvailablePurchases BEFORE withContext" )
216214 withContext(Dispatchers .IO ) {
217- android.util.Log .i(" HORIZON_QUERY" , " === getAvailablePurchases INSIDE withContext ===" )
218215 OpenIapLog .i(" === HORIZON getAvailablePurchases ===" , TAG )
219216
220217 val purchases = restorePurchasesHorizon(billingClient)
221- android.util.Log .i(" HORIZON_QUERY" , " Retrieved ${purchases.size} purchases from query" )
222218 OpenIapLog .i(" Retrieved ${purchases.size} total purchases (INAPP + SUBS)" , TAG )
223219
224220 // CRITICAL FIX: Merge with cached purchases
225221 val cachedPurchases = sharedPurchaseCache.values.toList()
226- android.util.Log .i(" HORIZON_QUERY" , " Cached purchases: ${cachedPurchases.size} " )
227222
228223 // Combine query results with cache, preferring query results
229224 val purchaseMap = mutableMapOf<String , Purchase >()
230225 cachedPurchases.forEach { purchaseMap[it.productId] = it }
231226 purchases.forEach { purchaseMap[it.productId] = it } // Override with fresh data
232227
233228 val allPurchases = purchaseMap.values.toList()
234- android.util.Log .i(" HORIZON_QUERY" , " Total purchases (query + cache): ${allPurchases.size} " )
235229
236230 allPurchases.forEachIndexed { index, purchase ->
237231 val txnId = when (purchase) {
238232 is dev.hyo.openiap.PurchaseAndroid -> purchase.transactionId
239233 else -> " N/A"
240234 }
241- android.util.Log .i(" HORIZON_QUERY" , " Purchase[$index ] productId=${purchase.productId} txnId=$txnId " )
242235 OpenIapLog .i(
243236 " [$index ] productId=${purchase.productId} " +
244237 " transactionId=$txnId " +
245238 " platform=${purchase.platform} " ,
246239 TAG
247240 )
248241 }
249- android.util.Log .i(" HORIZON_QUERY" , " === getAvailablePurchases END ===" )
250242 OpenIapLog .i(" === END getAvailablePurchases ===" , TAG )
251243 allPurchases
252244 }
253245 }
254246
255247 override val getActiveSubscriptions: QueryGetActiveSubscriptionsHandler = { subscriptionIds ->
256248 withContext(Dispatchers .IO ) {
257- android.util.Log .i(" HORIZON_QUERY" , " === getActiveSubscriptions START ===" )
258- android.util.Log .i(" HORIZON_QUERY" , " Requested IDs: $subscriptionIds " )
259249 OpenIapLog .i(" === HORIZON getActiveSubscriptions ===" , TAG )
260250 OpenIapLog .i(" Requested subscriptionIds: $subscriptionIds " , TAG )
261251
262252 val allPurchases = queryPurchasesHorizon(billingClient, BillingClient .ProductType .SUBS )
263- android.util.Log .i(" HORIZON_QUERY" , " Raw query returned ${allPurchases.size} SUBS purchases" )
264253 OpenIapLog .i(" Total SUBS purchases from query: ${allPurchases.size} " , TAG )
265254
266- allPurchases.forEachIndexed { index, purchase ->
267- android.util.Log .i(" HORIZON_QUERY" , " RawPurchase[$index ] productId=${purchase.productId} type=${purchase.javaClass.simpleName} " )
268- }
269-
270255 val androidPurchases = allPurchases.filterIsInstance<PurchaseAndroid >()
271- android.util.Log .i(" HORIZON_QUERY" , " Filtered to ${androidPurchases.size} PurchaseAndroid instances" )
272256 OpenIapLog .i(" PurchaseAndroid instances: ${androidPurchases.size} " , TAG )
273257
274258 val ids = subscriptionIds.orEmpty()
275259 val filtered = if (ids.isEmpty()) {
276- android.util.Log .i(" HORIZON_QUERY" , " No filter - returning all" )
277260 OpenIapLog .i(" No filter - returning all subscriptions" , TAG )
278261 androidPurchases
279262 } else {
280- android.util.Log .i(" HORIZON_QUERY" , " Filtering by IDs: $ids " )
281263 OpenIapLog .i(" Filtering by IDs: $ids " , TAG )
282264 androidPurchases.filter { it.productId in ids }
283265 }
284266
285- android.util.Log .i(" HORIZON_QUERY" , " After filtering: ${filtered.size} subscriptions" )
286267 OpenIapLog .i(" Filtered subscriptions count: ${filtered.size} " , TAG )
287268 val activeSubscriptions = filtered.map { it.toActiveSubscription() }
288269
289270 activeSubscriptions.forEachIndexed { index, sub ->
290- android.util.Log .i(" HORIZON_QUERY" , " ActiveSub[$index ] productId=${sub.productId} active=${sub.isActive} " )
291271 OpenIapLog .i(
292272 " [$index ] productId=${sub.productId} " +
293273 " isActive=${sub.isActive} " +
@@ -296,7 +276,6 @@ class OpenIapModule(
296276 )
297277 }
298278
299- android.util.Log .i(" HORIZON_QUERY" , " === getActiveSubscriptions END - returning ${activeSubscriptions.size} ===" )
300279 OpenIapLog .i(" === END getActiveSubscriptions ===" , TAG )
301280 activeSubscriptions
302281 }
@@ -309,6 +288,8 @@ class OpenIapModule(
309288 override val requestPurchase: MutationRequestPurchaseHandler = { props ->
310289 val purchases = withContext(Dispatchers .IO ) {
311290 val androidArgs = props.toAndroidPurchaseArgs()
291+ OpenIapLog .i(" === REQUEST PURCHASE: ${androidArgs.skus} ===" , TAG )
292+
312293 val activity = currentActivityRef?.get() ? : fallbackActivity
313294
314295 if (activity == null ) {
@@ -430,16 +411,10 @@ class OpenIapModule(
430411 }
431412
432413 val billingFlowParams = flowBuilder.build()
433- android.util.Log .i(TAG , " === LAUNCHING BILLING FLOW ===" )
434- android.util.Log .i(TAG , " - Is subscription? ${androidArgs.type == ProductQueryType .Subs } " )
435- android.util.Log .i(TAG , " - Has purchaseToken? ${! androidArgs.purchaseTokenAndroid.isNullOrBlank()} " )
436414
437415 // Run on UI thread as required by Android Billing API
438416 activity.runOnUiThread {
439417 val result = client.launchBillingFlow(activity, billingFlowParams)
440- android.util.Log .i(TAG , " === BILLING FLOW LAUNCHED ===" )
441- android.util.Log .i(TAG , " - Response code: ${result.responseCode} " )
442- android.util.Log .i(TAG , " - Debug message: ${result.debugMessage} " )
443418 OpenIapLog .d(" launchBillingFlow result: ${result.responseCode} - ${result.debugMessage} " , TAG )
444419
445420 if (result.responseCode != BillingClient .BillingResponseCode .OK ) {
@@ -468,7 +443,7 @@ class OpenIapModule(
468443 }
469444
470445 if (filtered.isNotEmpty()) {
471- android.util. Log .i( TAG , " Proactive query found ${filtered.size} purchases" )
446+ OpenIapLog .d( " Proactive query found ${filtered.size} purchases" , TAG )
472447 filtered.forEach { purchase ->
473448 purchaseUpdateListeners.forEach { listener ->
474449 runCatching { listener.onPurchaseUpdated(purchase) }
@@ -477,7 +452,7 @@ class OpenIapModule(
477452 currentPurchaseCallback?.invoke(Result .success(filtered))
478453 }
479454 } catch (e: Exception ) {
480- android.util. Log .e( TAG , " Error in proactive purchase query: ${e.message} " )
455+ OpenIapLog .e( " Error in proactive purchase query" , e, TAG )
481456 }
482457 }
483458 }
@@ -701,19 +676,14 @@ class OpenIapModule(
701676 }
702677
703678 override fun onPurchasesUpdated (result : BillingResult , purchases : List <HorizonPurchase >? ) {
704- // Log with Android Log to ensure it appears even if OpenIapLog fails
705- android.util.Log .wtf(" HORIZON_CALLBACK" , " onPurchasesUpdated START - responseCode=${result.responseCode} , count=${purchases?.size ? : 0 } " )
706-
707679 try {
708680 OpenIapLog .i(" === HORIZON onPurchasesUpdated ===" , TAG )
709681 OpenIapLog .i(" Response code: ${result.responseCode} " , TAG )
710- OpenIapLog .i(" Debug message: ${result.debugMessage} " , TAG )
711682 OpenIapLog .i(" Purchases count: ${purchases?.size ? : 0 } " , TAG )
712683
713684 purchases?.forEachIndexed { index, purchase ->
714685 val redactedToken = purchase.purchaseToken?.take(8 )?.plus(" …" )
715686 val redactedOrder = purchase.orderId?.take(8 )?.plus(" …" )
716- android.util.Log .i(" HORIZON_CALLBACK" , " Purchase[$index ] products=${purchase.products} token=$redactedToken " )
717687 OpenIapLog .i(
718688 " [HorizonPurchase $index ] productIds=${purchase.products} token=$redactedToken orderId=$redactedOrder " +
719689 " acknowledged=${purchase.isAcknowledged()} autoRenew=${purchase.isAutoRenewing()} " ,
@@ -725,7 +695,6 @@ class OpenIapModule(
725695 // When using DEFERRED replacement mode, purchases will be null
726696 // This is expected behavior - the change will take effect at next renewal
727697 if (purchases != null ) {
728- android.util.Log .i(" HORIZON_CALLBACK" , " Processing ${purchases.size} purchases" )
729698 OpenIapLog .i(" Processing ${purchases.size} successful purchases" , TAG )
730699
731700 val mapped = purchases.map { purchase ->
@@ -776,25 +745,22 @@ class OpenIapModule(
776745 OpenIapLog .i(" Purchase callback invoked" , TAG )
777746 } else {
778747 // Purchases is null - likely DEFERRED mode
779- android.util.Log .d(" HORIZON_CALLBACK" , " Purchase successful but purchases list is null (DEFERRED mode)" )
780748 OpenIapLog .d(" Purchase successful but purchases list is null (DEFERRED mode)" , TAG )
781749 currentPurchaseCallback?.let { cb ->
782750 currentPurchaseCallback = null
783751 cb.invoke(Result .success(emptyList()))
784752 }
785753 }
786754 } else {
787- android.util.Log .w(" HORIZON_CALLBACK" , " Purchase failed: code=${result.responseCode} " )
788755 OpenIapLog .w(" Purchase failed or cancelled: code=${result.responseCode} " , TAG )
789756 val error = OpenIapError .fromBillingResponseCode(result.responseCode, result.debugMessage)
790757 purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(error) } }
791758 currentPurchaseCallback?.invoke(Result .success(emptyList()))
792759 }
793760 currentPurchaseCallback = null
794- android.util.Log .i(" HORIZON_CALLBACK" , " onPurchasesUpdated END" )
795761 OpenIapLog .i(" === END onPurchasesUpdated ===" , TAG )
796762 } catch (e: Exception ) {
797- android.util. Log . e(" HORIZON_CALLBACK " , " Exception in onPurchasesUpdated" , e)
763+ OpenIapLog . e(" Exception in onPurchasesUpdated" , e, TAG )
798764 }
799765 }
800766
@@ -820,20 +786,36 @@ class OpenIapModule(
820786 }
821787 }
822788
823- private fun buildBillingClient () {
789+ /* *
790+ * Build BillingClient with the provided context.
791+ *
792+ * CRITICAL: Horizon SDK requires Activity to properly initialize OVRPlatform with returnComponent.
793+ * If Context (non-Activity) is provided, Horizon SDK will run in limited mode and may cause
794+ * NullPointerException during purchase flow.
795+ *
796+ * @param contextForBilling Activity (preferred) or Application Context (fallback)
797+ */
798+ private fun buildBillingClient (contextForBilling : Context ) {
799+ if (contextForBilling is Activity ) {
800+ OpenIapLog .d(" Building BillingClient with Activity" , TAG )
801+ } else {
802+ OpenIapLog .w(" Building BillingClient with Context (not Activity) - Horizon SDK will run in limited mode" , TAG )
803+ }
804+
824805 val pendingPurchasesParams = com.meta.horizon.billingclient.api.PendingPurchasesParams .newBuilder()
825806 .enableOneTimeProducts()
826807 .build()
827808
828809 val builder = BillingClient
829- .newBuilder(context )
810+ .newBuilder(contextForBilling )
830811 .setListener(this )
831812 .enablePendingPurchases(pendingPurchasesParams)
832813
833814 // Set app ID if available from manifest
834815 appId?.let { id ->
835816 if (id.isNotEmpty()) {
836817 builder.setAppId(id)
818+ OpenIapLog .d(" Horizon App ID set: $id " , TAG )
837819 }
838820 }
839821
0 commit comments