Skip to content

fix(horizon): initialize Billing with Activity to prevent Null Exception#27

Merged
hyochan merged 2 commits into
mainfrom
fix/horizon-nullpoint
Oct 27, 2025
Merged

fix(horizon): initialize Billing with Activity to prevent Null Exception#27
hyochan merged 2 commits into
mainfrom
fix/horizon-nullpoint

Conversation

@hyochan
Copy link
Copy Markdown
Member

@hyochan hyochan commented Oct 27, 2025

Resolve meta-quest/Meta-Spatial-SDK-Samples#82 (comment)

Summary by CodeRabbit

  • Chores

    • Updated Horizon platform SDK dependency to version 77.0.1.
  • Refactor

    • Improved billing client initialization and activity-aware handling for more reliable purchase flows.
    • Centralized and standardized internal logging to improve diagnostics and error visibility.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 27, 2025

Walkthrough

Horizon Meta Platform SDK version updated from 72 to 77.0.1 in build config. OpenIapModule.kt: replaced Android Log with OpenIapLog, deferred BillingClient construction to initConnection, and added Activity-aware context selection when building the BillingClient.

Changes

Cohort / File(s) Summary
Horizon SDK Dependency Bump
packages/google/openiap/build.gradle.kts
Updated horizonCompileOnly and horizonApi coordinates from com.meta.horizon.platform.ovr:android-platform-sdk:72 to com.meta.horizon.platform.ovr:android-platform-sdk:77.0.1.
Logging & BillingClient Initialization Refactor
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
Replaced android.util.Log calls with OpenIapLog; removed early BillingClient construction and now build BillingClient inside initConnection; use Activity when available (fallback to Context); updated logs for connection lifecycle, purchase flows, and error paths.

Sequence Diagram

sequenceDiagram
    participant Init as Module Init
    participant Module as OpenIapModule
    participant Activity as Activity (if any)
    participant Client as BillingClient

    Note over Init,Module: Old flow (pre-change)
    Init->>Module: construct module
    Module->>Client: build BillingClient immediately (Context)
    Module->>Module: log via Android Log

    Note over Init,Module: New flow (post-change)
    Init->>Module: construct module
    Module->>Module: defer BillingClient construction
    Module->>Module: initConnection invoked
    Module->>Activity: check for Activity
    alt Activity present
        Module->>Client: build BillingClient with Activity
        rect rgb(200,240,200)
            Client->>Module: connection started (OpenIapLog)
        end
    else Activity absent
        Module->>Client: build BillingClient with Context
        rect rgb(240,220,200)
            Client->>Module: connection started (OpenIapLog)
        end
    end
    Client->>Module: lifecycle logs via OpenIapLog
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify Activity availability and null-safety around building/rebuilding BillingClient in initConnection.
  • Confirm all Android Log usages in the Horizon flavor were replaced and no legacy logs remain.
  • Validate Horizon SDK 77.0.1 compatibility with existing BillingClient usage and any native-library loading implications (linked issue refactor: codebase improvements across docs, google, and gql packages #82).

Suggested labels

🛠 bugfix, 🤖 android

Poem

🐰 I hopped in code to make logs bright,

Activity found, Billing built right,
SDK bumped up to new sun beams,
Quiet logs and smoother streams,
A small hop forward — happy dreams!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR includes extensive logging refactoring—replacing all Android Log calls with OpenIapLog equivalents across Horizon flavor code paths—which goes beyond the stated objective of initializing BillingClient with Activity to prevent NullPointerException. While the Activity initialization and context selection changes are directly in scope, the comprehensive logging migration appears to be a separate code quality improvement that is not required by the issue or the PR title. This refactoring, though not unrelated to the component, expands the scope of changes beyond what is necessary to address the primary objective. Consider separating the logging refactoring from this PR into a dedicated code cleanup or refactoring PR. This PR should focus on the Activity-aware initialization and the dependency update needed to resolve the React Native compatibility issue, keeping the changeset focused and easier to review and test against the specific objectives.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues Check ❓ Inconclusive The linked issue #82 describes a React Native compatibility problem involving UnsatisfiedLinkError with wrapped C++ symbols (__wrap__ prefix mismatch) and requests diagnosis, root cause identification, and a React Native-compatible solution. However, the PR code changes—Activity-aware BillingClient initialization, OpenIapLog refactoring, and a dependency version bump from 72 to 77.0.1—do not explicitly address the symbol wrapping or native library loading objectives outlined in the issue. While the SDK version update may contain a fix for the symbol issue, the raw summary provides insufficient information to verify that these code changes fulfill the issue's stated requirements around diagnosing and resolving the React Native compatibility problem. To move forward, clarify whether the SDK version upgrade (72 → 77.0.1) addresses the React Native symbol wrapping issue and include a note in the PR description explaining how this update resolves the objectives in issue #82. Additionally, verify that the Activity initialization change and logging refactoring are necessary steps toward fixing the React Native compatibility problem or consider splitting refactoring changes into a separate PR focused on the core fix.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "fix(horizon): initialize Billing with Activity to prevent Null Exception" accurately reflects the primary architectural change shown in the OpenIapModule.kt summary: moving BillingClient construction from early initialization to the initConnection method where an Activity is guaranteed to be available. This change directly addresses NullPointerException prevention through Activity-aware context selection. While the title does not mention the dependency version upgrade from 72 to 77.0.1, it captures the main logic change that is the focus of the code modifications.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/horizon-nullpoint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hyochan hyochan changed the title fix(horizon): initialize BillingClient with Activity to prevent NullPointerException fix(horizon): initialize Billing with Activity to prevent Null Exception Oct 27, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)

835-914: Replace remaining android.util.Log calls with OpenIapLog for consistency.

The alternative billing methods still use android.util.Log.e() at lines 835, 845, 872, 882, 900, and 914, while the rest of the file has been migrated to OpenIapLog. This creates inconsistency in logging.

Apply these changes for consistency:

                 } catch (e: Exception) {
-                    Log.e(TAG, "Error checking alternative billing: ${e.message}")
+                    OpenIapLog.e("Error checking alternative billing", e, TAG)
                     cont.resumeWithException(e)
                 }
         } catch (e: Exception) {
-            Log.e(TAG, "Error in checkAlternativeBillingAvailability: ${e.message}")
+            OpenIapLog.e("Error in checkAlternativeBillingAvailability", e, TAG)
             false
         }
                 } catch (e: Exception) {
-                    Log.e(TAG, "Error showing alternative billing dialog: ${e.message}")
+                    OpenIapLog.e("Error showing alternative billing dialog", e, TAG)
                     cont.resumeWithException(e)
                 }
         } catch (e: Exception) {
-            Log.e(TAG, "Error in showAlternativeBillingInformationDialog: ${e.message}")
+            OpenIapLog.e("Error in showAlternativeBillingInformationDialog", e, TAG)
             false
         }
                 } catch (e: Exception) {
-                    Log.e(TAG, "Error creating alternative billing token: ${e.message}")
+                    OpenIapLog.e("Error creating alternative billing token", e, TAG)
                     cont.resumeWithException(e)
                 }
         } catch (e: Exception) {
-            Log.e(TAG, "Error in createAlternativeBillingReportingToken: ${e.message}")
+            OpenIapLog.e("Error in createAlternativeBillingReportingToken", e, TAG)
             null
         }
🧹 Nitpick comments (1)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)

787-817: Consider failing fast if Activity is unavailable for Horizon SDK.

The Activity-aware context selection is a good approach to fix the NullPointerException. However, the current implementation silently falls back to Context when no Activity is available (line 796), which may still cause the NPE the PR aims to fix.

If the Horizon SDK requires an Activity to function properly, consider failing fast instead of falling back:

 private fun buildBillingClient() {
     // CRITICAL: Use Activity if available, otherwise fall back to Context
     // Horizon SDK needs Activity to properly initialize OVRPlatform with returnComponent
     val activity = currentActivityRef?.get() ?: fallbackActivity
-    val contextForBilling: Context = activity ?: context
 
-    if (contextForBilling is Activity) {
+    if (activity != null) {
         OpenIapLog.d("Building BillingClient with Activity", TAG)
     } else {
-        OpenIapLog.w("No Activity available - Horizon SDK will initialize in limited mode", TAG)
+        OpenIapLog.e("No Activity available - Horizon SDK requires Activity for proper initialization", TAG)
+        throw OpenIapError.MissingCurrentActivity
     }
 
+    val contextForBilling: Context = activity
+
     val pendingPurchasesParams = com.meta.horizon.billingclient.api.PendingPurchasesParams.newBuilder()
         .enableOneTimeProducts()
         .build()

Alternatively, if Context fallback is intentionally supported for certain scenarios, document when "limited mode" is acceptable and what functionality is affected.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3a2f2b and 61d5900.

📒 Files selected for processing (2)
  • packages/google/openiap/build.gradle.kts (1 hunks)
  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-18T05:46:51.596Z
Learnt from: hyochan
PR: hyodotdev/openiap#17
File: packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt:620-629
Timestamp: 2025-10-18T05:46:51.596Z
Learning: In packages/google/openiap/src/main/java/**/*.kt (Android-only package): DO NOT add Android suffix to function names, even for Android-specific APIs. Exception: Only use Android suffix for cross-platform API types (e.g., ProductAndroid, PurchaseAndroid) that contrast with iOS types. Examples of correct naming: isHorizonEnvironment(context: Context), buildModule(context: Context), acknowledgePurchase(), consumePurchase().

Applied to files:

  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
🧬 Code graph analysis (1)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt (1)
  • buildBillingClient (904-1023)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Android
🔇 Additional comments (3)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (2)

82-85: LGTM - Logging consolidated.

The Android Log calls have been properly replaced with OpenIapLog equivalents for consistency.


113-143: LGTM - Connection initialization improved.

The logging has been properly consolidated to OpenIapLog, and the logic correctly rebuilds the BillingClient if it was previously destroyed by endConnection().

packages/google/openiap/build.gradle.kts (1)

93-94: Verify compatibility of SDK version 77.0.1 before merging.

The Meta Horizon Platform SDK version bump from 72 to 77.0.1 is a significant jump. No published changelog or breaking changes documentation is available for version 77.0.1, and no security advisories were found for this package.

Since this is being merged to fix a NullPointerException, verify that:

  • Version 77.0.1 is compatible with your codebase and does not introduce breaking API changes
  • The NPE issue is actually resolved with this version (or has another fix)
  • The updated SDK dependencies are tested on the target platforms

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (3)

789-823: Consider enforcing Activity requirement or deferring initialization.

The KDoc correctly states that Horizon SDK requires Activity and that providing a non-Activity Context causes "limited mode" and "may cause NullPointerException" (lines 792-795). However, the implementation only logs a warning (line 802) and proceeds with non-Activity context.

Given the PR objective is to "prevent Null Exception," consider one of these approaches:

  1. Enforce Activity requirement: Throw an exception or return early if contextForBilling !is Activity
  2. Defer initialization: Store the intent to initialize and retry when Activity becomes available via setActivity()
  3. Queue connection attempts: Block initConnection() until Activity is set, with a clear error message

The current approach documents the risk but doesn't prevent it.

Apply this diff to enforce Activity requirement:

 private fun buildBillingClient(contextForBilling: Context) {
-    if (contextForBilling is Activity) {
-        OpenIapLog.d("Building BillingClient with Activity", TAG)
-    } else {
-        OpenIapLog.w("Building BillingClient with Context (not Activity) - Horizon SDK will run in limited mode", TAG)
-    }
+    if (contextForBilling !is Activity) {
+        OpenIapLog.e("FATAL: Horizon SDK requires Activity context. BillingClient initialization aborted.", tag = TAG)
+        throw OpenIapError.MissingCurrentActivity
+    }
+    OpenIapLog.d("Building BillingClient with Activity", TAG)

360-360: Replace remaining android.util.Log call with OpenIapLog for consistency.

Line 360 still uses android.util.Log.i while the rest of the file has been migrated to OpenIapLog. This inconsistency undermines the centralized logging strategy.

Apply this diff:

-                            android.util.Log.i(TAG, "BILLING_FLOW_PARAM: SKU=${productDetails.productId}, resolvedOfferToken=$resolved")
+                            OpenIapLog.i("BILLING_FLOW_PARAM: SKU=${productDetails.productId}, resolvedOfferToken=$resolved", TAG)

826-923: Complete logging migration in alternative billing methods.

Lines 841, 851, 878, 888, 906, and 920 still use Log.e instead of OpenIapLog.e. The rest of the file has been migrated to OpenIapLog, but these alternative billing methods were missed.

Apply these diffs to complete the migration:

 } catch (e: Exception) {
-    Log.e(TAG, "Error checking alternative billing: ${e.message}")
+    OpenIapLog.e("Error checking alternative billing: ${e.message}", e, TAG)
     cont.resumeWithException(e)
 } catch (e: Exception) {
-    Log.e(TAG, "Error in checkAlternativeBillingAvailability: ${e.message}")
+    OpenIapLog.e("Error in checkAlternativeBillingAvailability: ${e.message}", e, TAG)
     false
 } catch (e: Exception) {
-    Log.e(TAG, "Error showing alternative billing dialog: ${e.message}")
+    OpenIapLog.e("Error showing alternative billing dialog: ${e.message}", e, TAG)
     cont.resumeWithException(e)
 } catch (e: Exception) {
-    Log.e(TAG, "Error in showAlternativeBillingInformationDialog: ${e.message}")
+    OpenIapLog.e("Error in showAlternativeBillingInformationDialog: ${e.message}", e, TAG)
     false
 } catch (e: Exception) {
-    Log.e(TAG, "Error creating alternative billing token: ${e.message}")
+    OpenIapLog.e("Error creating alternative billing token: ${e.message}", e, TAG)
     cont.resumeWithException(e)
 } catch (e: Exception) {
-    Log.e(TAG, "Error in createAlternativeBillingReportingToken: ${e.message}")
+    OpenIapLog.e("Error in createAlternativeBillingReportingToken: ${e.message}", e, TAG)
     null
♻️ Duplicate comments (1)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)

101-104: Race condition still not resolved—Activity may be unavailable when initConnection() is called.

While deferring BillingClient construction is correct, the race condition identified in the previous review persists. In React Native, context is a ReactApplicationContext (Application, not Activity), so fallbackActivity (line 94) is null. When initConnection() is invoked before setActivity(), line 118 falls back to context—an Application context—which Horizon SDK requires to be an Activity (line 792-794 documentation confirms this).

This means the NPE that this PR aims to fix (per PR objectives) can still occur if the initialization sequence is: new OpenIapModule()initConnection()setActivity().

Recommendation: Add a guard in initConnection() to defer or reject connection attempts when Activity is unavailable, or register an ActivityEventListener in the React Native bridge to capture Activity immediately upon availability.

🧹 Nitpick comments (1)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)

5-5: Remove unused android.util.Log import after completing migration.

Once all Log.e and Log.i calls are replaced with OpenIapLog (see previous comments), this import should be removed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61d5900 and 8385692.

📒 Files selected for processing (1)
  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-18T05:46:51.596Z
Learnt from: hyochan
PR: hyodotdev/openiap#17
File: packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt:620-629
Timestamp: 2025-10-18T05:46:51.596Z
Learning: In packages/google/openiap/src/main/java/**/*.kt (Android-only package): DO NOT add Android suffix to function names, even for Android-specific APIs. Exception: Only use Android suffix for cross-platform API types (e.g., ProductAndroid, PurchaseAndroid) that contrast with iOS types. Examples of correct naming: isHorizonEnvironment(context: Context), buildModule(context: Context), acknowledgePurchase(), consumePurchase().

Applied to files:

  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
🧬 Code graph analysis (1)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt (1)
  • buildBillingClient (904-1023)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Android
🔇 Additional comments (1)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)

113-141: Logging migration looks good.

The consistent use of OpenIapLog improves debugging and aligns with the codebase's logging strategy.

@hyochan hyochan merged commit cdb5fd0 into main Oct 27, 2025
5 checks passed
@hyochan hyochan deleted the fix/horizon-nullpoint branch October 27, 2025 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Horizon Platform SDK: React Native Compatibility Issue

1 participant