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
Original file line number Diff line number Diff line change
Expand Up @@ -1082,30 +1082,70 @@ func format_date(timestamp: int) -> String:
<p>
<strong>Available replacement modes:</strong>
</p>
<ul>
<li>
<code>WITH_TIME_PRORATION</code> - Immediate change
with prorated credit
</li>
<li>
<code>CHARGE_PRORATED_PRICE</code> - Immediate change,
charge difference (upgrade only)
</li>
<li>
<code>WITHOUT_PRORATION</code> - Immediate change, no
proration
</li>
<li>
<code>CHARGE_FULL_PRICE</code> - Immediate change,
charge full price
</li>
<li>
<code>DEFERRED</code> - Change at next billing cycle
</li>
<li>
<code>KEEP_EXISTING</code> - Keep the existing payment schedule unchanged (8.1.0+)
</li>
</ul>
<div style={{ overflowX: 'auto' }}>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
marginTop: '0.5rem',
marginBottom: '0.5rem',
fontSize: '0.875rem',
}}
>
<thead>
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
<th style={{ textAlign: 'left', padding: '0.5rem' }}>Mode</th>
<th style={{ textAlign: 'center', padding: '0.5rem' }}>Legacy API</th>
<th style={{ textAlign: 'center', padding: '0.5rem' }}>8.1.0+ API</th>
<th style={{ textAlign: 'left', padding: '0.5rem' }}>Description</th>
</tr>
</thead>
<tbody>
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
<td style={{ padding: '0.25rem 0.5rem' }}><code>WITH_TIME_PRORATION</code></td>
<td style={{ textAlign: 'center' }}>1</td>
<td style={{ textAlign: 'center' }}>1</td>
<td style={{ padding: '0.25rem 0.5rem' }}>Immediate change with prorated credit</td>
</tr>
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
<td style={{ padding: '0.25rem 0.5rem' }}><code>CHARGE_PRORATED_PRICE</code></td>
<td style={{ textAlign: 'center' }}>2</td>
<td style={{ textAlign: 'center' }}>2</td>
<td style={{ padding: '0.25rem 0.5rem' }}>Immediate change, charge difference (upgrade only)</td>
</tr>
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
<td style={{ padding: '0.25rem 0.5rem' }}><code>WITHOUT_PRORATION</code></td>
<td style={{ textAlign: 'center' }}>3</td>
<td style={{ textAlign: 'center' }}>3</td>
<td style={{ padding: '0.25rem 0.5rem' }}>Immediate change, no proration</td>
</tr>
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
<td style={{ padding: '0.25rem 0.5rem' }}><code>CHARGE_FULL_PRICE</code></td>
<td style={{ textAlign: 'center' }}>5</td>
<td style={{ textAlign: 'center' }}>4</td>
<td style={{ padding: '0.25rem 0.5rem' }}>Immediate change, charge full price</td>
</tr>
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
<td style={{ padding: '0.25rem 0.5rem' }}><code>DEFERRED</code></td>
<td style={{ textAlign: 'center' }}>6</td>
<td style={{ textAlign: 'center' }}>5</td>
<td style={{ padding: '0.25rem 0.5rem' }}>Change at next billing cycle</td>
</tr>
<tr>
<td style={{ padding: '0.25rem 0.5rem' }}><code>KEEP_EXISTING</code></td>
<td style={{ textAlign: 'center' }}>—</td>
<td style={{ textAlign: 'center' }}>6</td>
<td style={{ padding: '0.25rem 0.5rem' }}>Keep existing payment schedule (8.1.0+ only)</td>
</tr>
</tbody>
</table>
</div>

<p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
<strong>Note:</strong> Legacy API refers to <code>SubscriptionUpdateParams.ReplacementMode</code>,
8.1.0+ API refers to <code>SubscriptionProductReplacementParams.ReplacementMode</code>.
The integer values differ for CHARGE_FULL_PRICE and DEFERRED between APIs.
</p>

<p>
<strong>Note:</strong> If you don't specify a replacement
Expand Down Expand Up @@ -1307,7 +1347,7 @@ if current_sub:

<ol>
<li>
<strong>Use DEFERRED replacement mode (value: 6)</strong>
<strong>Use DEFERRED replacement mode</strong> (Legacy API: 6, 8.1.0+ API: 5)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</li>
<li>No immediate charge to the user</li>
<li>User keeps premium access until current period ends</li>
Expand All @@ -1325,7 +1365,7 @@ if current_sub:
>
<p>
<strong>
When using DEFERRED replacement mode (6), the purchase
When using DEFERRED replacement mode, the purchase
callback completes successfully with an empty purchase
list.
</strong>{' '}
Expand Down Expand Up @@ -1377,7 +1417,7 @@ if (premiumPurchase) {
await requestPurchase({
sku: 'basic_monthly',
purchaseToken: premiumPurchase.purchaseToken,
replacementMode: 6, // DEFERRED - Change at renewal
replacementMode: 6, // DEFERRED (Legacy API value) - Change at renewal
});

console.log('✅ Downgrade scheduled for next billing cycle');
Expand All @@ -1398,7 +1438,7 @@ premiumPurchase?.let { purchase ->
android {
skus = listOf("basic_monthly")
purchaseToken = purchase.purchaseToken
replacementMode = 6 // DEFERRED - Change at renewal
replacementMode = 6 // DEFERRED (Legacy API value) - Change at renewal
}
}

Expand All @@ -1422,7 +1462,7 @@ if (premiumPurchase != null) {
google: RequestPurchaseAndroidProps(
skus: ['basic_monthly'],
purchaseToken: premiumPurchase.purchaseToken,
replacementMode: 6, // DEFERRED - Change at renewal
replacementMode: 6, // DEFERRED (Legacy API value) - Change at renewal
),
),
),
Expand Down Expand Up @@ -1450,7 +1490,7 @@ if premium_purchase:
props.request.google = RequestPurchaseAndroidProps.new()
props.request.google.skus = ["basic_monthly"]
props.request.google.purchase_token = premium_purchase.purchase_token
props.request.google.replacement_mode = 6 # DEFERRED - Change at renewal
props.request.google.replacement_mode = 6 # DEFERRED (Legacy API value) - Change at renewal
props.type = ProductType.SUBS

await iap.request_purchase(props)
Expand Down Expand Up @@ -1707,11 +1747,11 @@ for purchase in purchases:
override the default configured in Google Play Console
</li>
<li>
<strong>Use WITH_TIME_PRORATION (1) for upgrades</strong> to
<strong>Use WITH_TIME_PRORATION for upgrades</strong> to
give users credit for unused time
</li>
<li>
<strong>Use DEFERRED (6) for downgrades</strong> to let
<strong>Use DEFERRED for downgrades</strong> to let
users keep premium features until period ends
</li>
<li>
Expand Down Expand Up @@ -1759,7 +1799,7 @@ async function changeSubscription(
// Choose appropriate replacement mode
const replacementMode = isUpgrade
? 1 // WITH_TIME_PRORATION - Upgrade: give credit
: 6; // DEFERRED - Downgrade: change at renewal
: 6; // DEFERRED (Legacy API value) - Downgrade: change at renewal

try {
await requestPurchase({
Expand Down Expand Up @@ -1807,7 +1847,7 @@ suspend fun changeSubscription(
val replacementMode = if (isUpgrade) {
1 // WITH_TIME_PRORATION - Upgrade: give credit
} else {
6 // DEFERRED - Downgrade: change at renewal
6 // DEFERRED (Legacy API value) - Downgrade: change at renewal
}

try {
Expand Down Expand Up @@ -1858,7 +1898,7 @@ Future<void> changeSubscription(
// Choose appropriate replacement mode
final replacementMode = isUpgrade
? 1 // WITH_TIME_PRORATION - Upgrade: give credit
: 6; // DEFERRED - Downgrade: change at renewal
: 6; // DEFERRED (Legacy API value) - Downgrade: change at renewal

try {
await FlutterInappPurchase.instance.requestPurchase(
Expand Down Expand Up @@ -1908,7 +1948,7 @@ func change_subscription(new_sku: String, is_upgrade: bool) -> void:
# Choose appropriate replacement mode
var replacement_mode = 1 if is_upgrade else 6
# 1 = WITH_TIME_PRORATION - Upgrade: give credit
# 6 = DEFERRED - Downgrade: change at renewal
# 6 = DEFERRED (Legacy API value) - Downgrade: change at renewal

var props = RequestPurchaseProps.new()
props.request = RequestPurchasePropsByPlatforms.new()
Expand Down
43 changes: 43 additions & 0 deletions packages/docs/src/pages/docs/updates/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,49 @@ function Notes() {
useScrollToHash();

const allNotes: Note[] = [
// GQL 1.3.15 / Google 1.3.27 / Apple 1.3.13 - Jan 21, 2026
{
id: 'gql-1-3-15-google-1-3-27-apple-1-3-13',
date: new Date('2026-01-21'),
element: (
<div key="gql-1-3-15-google-1-3-27-apple-1-3-13" style={noteCardStyle}>
<AnchorLink id="gql-1-3-15-google-1-3-27-apple-1-3-13" level="h4">
📅 openiap-gql v1.3.15 / openiap-google v1.3.27 / openiap-apple v1.3.13 - Bug Fix
</AnchorLink>

<p><strong>Android - Fix SubscriptionProductReplacementParams ReplacementMode Mapping:</strong></p>
<p>
Fixed incorrect <code>replacementModeConstant</code> mapping in <code>applySubscriptionProductReplacementParams</code>.
The function was using values from the legacy <code>SubscriptionUpdateParams.ReplacementMode</code> API instead of
the new <code>SubscriptionProductReplacementParams.ReplacementMode</code> API (Billing Library 8.1.0+).
</p>
<p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
Issue: <a href="https://github.com/hyodotdev/openiap/issues/71" target="_blank" rel="noopener noreferrer">#71</a>
</p>

<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '0.5rem', marginBottom: '0.5rem' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--border-color)' }}>
<th style={{ textAlign: 'left', padding: '0.5rem' }}>Mode</th>
<th style={{ textAlign: 'center', padding: '0.5rem' }}>Before (Wrong)</th>
<th style={{ textAlign: 'center', padding: '0.5rem' }}>After (Correct)</th>
</tr>
</thead>
<tbody>
<tr><td style={{ padding: '0.25rem 0.5rem' }}>CHARGE_FULL_PRICE</td><td style={{ textAlign: 'center' }}>5</td><td style={{ textAlign: 'center' }}>4</td></tr>
<tr><td style={{ padding: '0.25rem 0.5rem' }}>DEFERRED</td><td style={{ textAlign: 'center' }}>6</td><td style={{ textAlign: 'center' }}>5</td></tr>
<tr><td style={{ padding: '0.25rem 0.5rem' }}>KEEP_EXISTING</td><td style={{ textAlign: 'center' }}>7</td><td style={{ textAlign: 'center' }}>6</td></tr>
</tbody>
</table>

<p><strong>References:</strong></p>
<ul>
<li><a href="https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode" target="_blank" rel="noopener noreferrer">SubscriptionProductReplacementParams.ReplacementMode (Billing 8.1.0+)</a></li>
<li><a href="https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode" target="_blank" rel="noopener noreferrer">SubscriptionUpdateParams.ReplacementMode (Legacy)</a></li>
</ul>
</div>
),
},
// GQL 1.3.14 / Google 1.3.25 / Apple 1.3.13 - Jan 19, 2026
{
id: 'gql-1-3-14-google-1-3-25-apple-1-3-13',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.hyo.openiap

/**
* Extension function to convert SubscriptionReplacementModeAndroid enum to
* BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode constants.
*
* Reference (Billing Library 8.1.0+):
* https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode
*
* Note: These constants differ from the legacy SubscriptionUpdateParams.ReplacementMode API.
* See: https://github.com/hyodotdev/openiap/issues/71
*
* @return The integer constant for SubscriptionProductReplacementParams.ReplacementMode
*/
internal fun SubscriptionReplacementModeAndroid.toReplacementModeConstant(): Int {
return when (this) {
SubscriptionReplacementModeAndroid.UnknownReplacementMode -> 0
SubscriptionReplacementModeAndroid.WithTimeProration -> 1
SubscriptionReplacementModeAndroid.ChargeProratedPrice -> 2
SubscriptionReplacementModeAndroid.WithoutProration -> 3
SubscriptionReplacementModeAndroid.ChargeFullPrice -> 4
SubscriptionReplacementModeAndroid.Deferred -> 5
SubscriptionReplacementModeAndroid.KeepExisting -> 6
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1605,16 +1605,8 @@ class OpenIapModule(
params: SubscriptionProductReplacementParamsAndroid
) {
try {
// Convert our enum to BillingClient replacement mode constant
val replacementModeConstant = when (params.replacementMode) {
SubscriptionReplacementModeAndroid.UnknownReplacementMode -> 0
SubscriptionReplacementModeAndroid.WithTimeProration -> 1
SubscriptionReplacementModeAndroid.ChargeProratedPrice -> 2
SubscriptionReplacementModeAndroid.WithoutProration -> 3
SubscriptionReplacementModeAndroid.Deferred -> 6
SubscriptionReplacementModeAndroid.ChargeFullPrice -> 5
SubscriptionReplacementModeAndroid.KeepExisting -> 7 // New in 8.1.0
}
// Convert our enum to BillingClient SubscriptionProductReplacementParams.ReplacementMode constant
val replacementModeConstant = params.replacementMode.toReplacementModeConstant()

// Build SubscriptionProductReplacementParams using reflection
// Note: SubscriptionProductReplacementParams is nested under ProductDetailsParams (Billing Library 8.1.0+)
Expand Down
Loading
Loading