Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import dev.hyo.openiap.BillingProgramAndroid as OpenIapBillingProgramAndroid
import dev.hyo.openiap.LaunchExternalLinkParamsAndroid as OpenIapLaunchExternalLinkParams
import dev.hyo.openiap.ExternalLinkLaunchModeAndroid as OpenIapExternalLinkLaunchMode
import dev.hyo.openiap.ExternalLinkTypeAndroid as OpenIapExternalLinkType
import dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener
import dev.hyo.openiap.store.OpenIapStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -78,6 +79,7 @@ class HybridRnIap : HybridRnIapSpec() {
private val purchaseErrorListeners = mutableListOf<(NitroPurchaseResult) -> Unit>()
private val promotedProductListenersIOS = mutableListOf<(NitroProduct) -> Unit>()
private val userChoiceBillingListenersAndroid = mutableListOf<(UserChoiceBillingDetails) -> Unit>()
private val developerProvidedBillingListenersAndroid = mutableListOf<(DeveloperProvidedBillingDetailsAndroid) -> Unit>()
private var listenersAttached = false
private var isInitialized = false
private var initDeferred: CompletableDeferred<Boolean>? = null
Expand Down Expand Up @@ -168,19 +170,37 @@ class HybridRnIap : HybridRnIapSpec() {
sendUserChoiceBilling(nitroDetails)
}.onFailure { RnIapLog.failure("userChoiceBillingListener", it) }
})
// Developer Provided Billing listener (External Payments - 8.3.0+)
openIap.addDeveloperProvidedBillingListener(OpenIapDeveloperProvidedBillingListener { details ->
runCatching {
RnIapLog.result(
"developerProvidedBillingListener",
mapOf("token" to details.externalTransactionToken)
)
val nitroDetails = DeveloperProvidedBillingDetailsAndroid(
externalTransactionToken = details.externalTransactionToken
)
sendDeveloperProvidedBilling(nitroDetails)
}.onFailure { RnIapLog.failure("developerProvidedBillingListener", it) }
})
RnIapLog.result("listeners.attach", "attached")
}

// We created it above; reuse the shared instance
val deferred = initDeferred!!
try {
// Convert Nitro config to OpenIAP config
// Note: enableBillingProgramAndroid is passed to OpenIapInitConnectionConfig
// which handles enabling the billing program internally
val openIapConfig = config?.let {
OpenIapInitConnectionConfig(
alternativeBillingModeAndroid = when (it.alternativeBillingModeAndroid) {
com.margelo.nitro.iap.AlternativeBillingModeAndroid.USER_CHOICE -> dev.hyo.openiap.AlternativeBillingModeAndroid.UserChoice
com.margelo.nitro.iap.AlternativeBillingModeAndroid.ALTERNATIVE_ONLY -> dev.hyo.openiap.AlternativeBillingModeAndroid.AlternativeOnly
else -> null
},
enableBillingProgramAndroid = config.enableBillingProgramAndroid?.let { program ->
mapBillingProgram(program)
}
)
}
Expand Down Expand Up @@ -1424,6 +1444,25 @@ class HybridRnIap : HybridRnIapSpec() {
}
}

// Developer Provided Billing listener (External Payments - 8.3.0+)
override fun addDeveloperProvidedBillingListenerAndroid(listener: (DeveloperProvidedBillingDetailsAndroid) -> Unit) {
synchronized(developerProvidedBillingListenersAndroid) {
developerProvidedBillingListenersAndroid.add(listener)
}
}

override fun removeDeveloperProvidedBillingListenerAndroid(listener: (DeveloperProvidedBillingDetailsAndroid) -> Unit) {
synchronized(developerProvidedBillingListenersAndroid) {
developerProvidedBillingListenersAndroid.remove(listener)
}
}

private fun sendDeveloperProvidedBilling(details: DeveloperProvidedBillingDetailsAndroid) {
synchronized(developerProvidedBillingListenersAndroid) {
developerProvidedBillingListenersAndroid.forEach { it(details) }
}
}

// -------------------------------------------------------------------------
// Billing Programs API (Android 8.2.0+)
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -1526,6 +1565,7 @@ class HybridRnIap : HybridRnIapSpec() {
BillingProgramAndroid.UNSPECIFIED -> OpenIapBillingProgramAndroid.Unspecified
BillingProgramAndroid.EXTERNAL_CONTENT_LINK -> OpenIapBillingProgramAndroid.ExternalContentLink
BillingProgramAndroid.EXTERNAL_OFFER -> OpenIapBillingProgramAndroid.ExternalOffer
BillingProgramAndroid.EXTERNAL_PAYMENTS -> OpenIapBillingProgramAndroid.ExternalPayments
}
}

Expand Down
119 changes: 119 additions & 0 deletions docs/blog/2025-12-28-release-14.6.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
slug: release-14.6.4
title: Release 14.6.4 - Google Play Billing 8.3.0 External Payments Support
authors: [hyochan]
tags: [release, android, external-payments, billing-library]
description: React Native IAP 14.6.4 adds support for Google Play Billing Library 8.3.0 External Payments program, enabling side-by-side payment choice in Japan.
date: 2025-12-28
---

# React Native IAP 14.6.4

14.6.4 adds support for the **External Payments** program introduced in Google Play Billing Library 8.3.0. This new feature enables developers to offer a side-by-side choice between Google Play Billing and their external payment option directly in the purchase flow.

<!-- truncate -->

## What's New

### External Payments Program (Japan Only)

Google Play Billing Library 8.3.0 introduces the External Payments program, which presents a **side-by-side choice** between Google Play Billing and the developer's external payment option directly in the purchase dialog.

This differs from the existing User Choice Billing in that both options appear together in the same dialog, rather than requiring a separate selection dialog.

#### New APIs

- **`BillingProgramAndroid.EXTERNAL_PAYMENTS`** - New billing program type for external payments
- **`DeveloperBillingOptionParamsAndroid`** - Configure external payment option in purchase flow
- **`DeveloperBillingLaunchModeAndroid`** - How to launch the external payment link
- **`DeveloperProvidedBillingDetailsAndroid`** - Contains `externalTransactionToken` when user selects developer billing
- **`developerProvidedBillingListenerAndroid`** - New listener for when user selects developer billing
- **`developerBillingOption`** - New field in `RequestPurchaseAndroidProps` and `RequestSubscriptionAndroidProps`

#### New Event

- **`IapEvent.DeveloperProvidedBillingAndroid`** - Fired when user selects developer billing in External Payments flow

### Usage Example

```typescript
import {
enableBillingProgramAndroid,
developerProvidedBillingListenerAndroid,
requestPurchase,
initConnection,
} from 'react-native-iap';

// Option A: Enable External Payments via initConnection config (Recommended)
await initConnection({
enableBillingProgramAndroid: 'external-payments',
});

// Option B: Enable manually BEFORE initConnection
enableBillingProgramAndroid('external-payments');
await initConnection();

// Set up listener for when user selects developer billing
const subscription = developerProvidedBillingListenerAndroid((details) => {
console.log('User selected developer billing');
console.log('Token:', details.externalTransactionToken);

// Process payment through your external payment system
processExternalPayment();

// Report to Google Play backend within 24 hours
reportToGooglePlay(details.externalTransactionToken);
});

// Request purchase with developer billing option
await requestPurchase({
request: {
google: {
skus: ['premium_monthly'],
developerBillingOption: {
billingProgram: 'external-payments',
linkUri: 'https://your-website.com/payment',
launchMode: 'launch-in-external-browser-or-app',
},
},
},
type: 'subs',
});
```

### Key Differences from User Choice Billing

| Feature | User Choice Billing | External Payments |
|---------|-------------------|-------------------|
| Billing Library | 7.0+ | 8.3.0+ |
| Availability | Eligible regions | Japan only |
| UI | Separate dialog | Side-by-side in purchase dialog |
| Setup Mode | `alternativeBillingModeAndroid: 'user-choice'` | `enableBillingProgramAndroid('external-payments')` |
| Listener | `userChoiceBillingListenerAndroid` | `developerProvidedBillingListenerAndroid` |

## OpenIAP Updates

- **openiap-google**: [1.3.16 → 1.3.19](https://github.com/hyodotdev/openiap/compare/google-v1.3.16...google-v1.3.19) - Google Play Billing 8.3.0 External Payments support
- **openiap-gql**: [1.3.8 → 1.3.10](https://github.com/hyodotdev/openiap/compare/gql-v1.3.8...gql-v1.3.10) - External Payments types, `enableBillingProgramAndroid` in InitConnectionConfig
- **openiap-apple**: [1.3.7 → 1.3.8](https://github.com/hyodotdev/openiap/compare/apple-v1.3.7...apple-v1.3.8) - Auto connection management improvements

## Important Notes

- External Payments is currently **only available in Japan**
- Can enable via `initConnection({ enableBillingProgramAndroid: 'external-payments' })` (recommended) or `enableBillingProgramAndroid('external-payments')` before initConnection
- External transaction token must be reported to Google Play within **24 hours**
- If user selects Google Play billing, `purchaseUpdatedListener` fires as normal

## Installation

```bash
npm install react-native-iap react-native-nitro-modules
# or
yarn add react-native-iap react-native-nitro-modules
# or
bun add react-native-iap react-native-nitro-modules
```

Check out the [Alternative Billing Guide](/docs/guides/alternative-billing#external-payments-api-830---japan-only) for detailed documentation.

Questions or feedback? Reach out via [GitHub issues](https://github.com/hyochan/react-native-iap/issues).
85 changes: 85 additions & 0 deletions docs/docs/api/methods/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,91 @@ Purchases can be in different states:

Handle each state appropriately in your purchase listener.

## developerProvidedBillingListenerAndroid() (Android only)

Android-only listener for External Payments events (Google Play Billing Library 8.3.0+). This fires when a user selects the developer's billing option instead of Google Play billing in the External Payments side-by-side choice dialog. This feature is currently only available in Japan.

```tsx
import {
initConnection,
developerProvidedBillingListenerAndroid,
enableBillingProgramAndroid,
} from 'react-native-iap';
import {Platform} from 'react-native';

const setupExternalPaymentsListener = async () => {
if (Platform.OS !== 'android') return;

// Enable External Payments program BEFORE initConnection
enableBillingProgramAndroid('external-payments');

// Initialize connection
await initConnection();

const subscription = developerProvidedBillingListenerAndroid((details) => {
console.log('User selected developer billing');
console.log('Token:', details.externalTransactionToken);

handleExternalPayment(details);
});

// Clean up listener when component unmounts
return () => {
if (subscription) {
subscription.remove();
}
};
};

const handleExternalPayment = async (details) => {
try {
// Step 1: Process payment through your external payment system
const paymentResult = await processExternalPayment();

if (!paymentResult.success) {
console.error('External payment failed');
return;
}

// Step 2: Report token to Google Play backend within 24 hours
await reportTokenToGooglePlay({
token: details.externalTransactionToken,
paymentResult,
});

console.log('External payment completed successfully');
} catch (error) {
console.error('Error handling external payment:', error);
}
};
```

**Parameters:**

- `callback` (function): Function to call when user selects developer billing
- `details` (DeveloperProvidedBillingDetailsAndroid): The developer billing details
- `externalTransactionToken` (string): Token that must be reported to Google within 24 hours

**Returns:** Subscription object with `remove()` method

**Platform:** Android only (requires Google Play Billing Library 8.3.0+, Japan only)

**Important:**

- Only available in the External Payments program (Japan only)
- Must enable `external-payments` program BEFORE `initConnection()`
- Token must be reported to Google Play backend within 24 hours
- If user selects Google Play billing instead, `purchaseUpdatedListener` will fire as normal

### Difference from User Choice Billing

| Feature | User Choice Billing | External Payments |
|---------|-------------------|-------------------|
| Billing Library | 7.0+ | 8.3.0+ |
| Availability | Eligible regions | Japan only |
| UI | Separate dialog | Side-by-side in purchase dialog |
| Listener | `userChoiceBillingListenerAndroid` | `developerProvidedBillingListenerAndroid` |

## Alternative: useIAP Hook

For simpler usage, consider using the `useIAP` hook which automatically manages listeners:
Expand Down
73 changes: 73 additions & 0 deletions docs/docs/guides/alternative-billing.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,79 @@ if (success) {
- **`link-to-digital-content-offer`**: Link to a digital content offer
- **`link-to-app-download`**: Link to download an app

### External Payments API (8.3.0+) - Japan Only

:::tip New in 14.6.4
Google Play Billing Library 8.3.0 introduces the External Payments program, which presents a **side-by-side choice** between Google Play Billing and the developer's external payment option directly in the purchase flow. This is currently only available in Japan.
:::

External Payments differs from User Choice Billing in that it shows both options side-by-side within the same dialog, rather than requiring a separate dialog.

```typescript
import {
enableBillingProgramAndroid,
developerProvidedBillingListenerAndroid,
requestPurchase,
initConnection,
} from 'react-native-iap';

// Option A: Enable External Payments via initConnection config (Recommended)
await initConnection({
enableBillingProgramAndroid: 'external-payments',
});

// Option B: Enable manually BEFORE initConnection
enableBillingProgramAndroid('external-payments');
await initConnection();

// Set up listener for when user selects developer billing
const subscription = developerProvidedBillingListenerAndroid((details) => {
console.log('User selected developer billing');
console.log('External transaction token:', details.externalTransactionToken);

// Process payment through your external payment system
processExternalPayment(details.externalTransactionToken);

// Report to Google Play backend within 24 hours
reportToGooglePlay(details.externalTransactionToken);
});

// Request purchase with developer billing option
await requestPurchase({
request: {
google: {
skus: ['premium_monthly'],
developerBillingOption: {
billingProgram: 'external-payments',
linkUri: 'https://your-website.com/payment',
launchMode: 'launch-in-external-browser-or-app',
},
},
},
type: 'subs',
});

// Clean up
subscription.remove();
```

#### Key Differences: External Payments vs User Choice Billing

| Feature | User Choice Billing | External Payments |
|---------|-------------------|-------------------|
| Billing Library | 7.0+ | 8.3.0+ |
| Availability | Eligible regions | Japan only |
| UI | Separate dialog | Side-by-side in purchase dialog |
| Setup (Config) | `alternativeBillingModeAndroid: 'user-choice'` | `enableBillingProgramAndroid: 'external-payments'` |
| Setup (Manual) | `initConnection({ alternativeBillingModeAndroid: 'user-choice' })` | `enableBillingProgramAndroid('external-payments')` before initConnection |
| Listener | `userChoiceBillingListenerAndroid` | `developerProvidedBillingListenerAndroid` |
| Purchase Props | `useAlternativeBilling: true` | `developerBillingOption: {...}` |

#### Developer Billing Launch Modes

- **`launch-in-external-browser-or-app`**: Google Play launches the external URL directly
- **`caller-will-launch-link`**: Your app handles launching the URL after Play returns control

### Legacy Alternative Billing APIs (Pre-8.2.0)

For apps using older Billing Library versions, the legacy APIs are still supported but deprecated:
Expand Down
Loading