Skip to main content

3 posts tagged with "Billing"

Billing related updates

View All Tags

3.4.4 - Android Input Type Field Naming Simplification

· 2 min read
Hyo
Expo IAP Maintainer

This release simplifies field naming in Android input types (RequestPurchaseAndroidProps and RequestSubscriptionAndroidProps). Since these types are already Android-specific, their fields no longer need the Android suffix.

Breaking Changes

Simplified Field Names in Android Input Types

Fields inside platform-specific input types no longer require the platform suffix. The parent type name already indicates the platform context.

Why this change?

When you write google: { offerToken: "..." }, the google key already tells you this is Android-specific. Adding Android suffix to fields inside is redundant:

// Redundant - we know it's Android from the parent
google: { offerTokenAndroid: "..." }

// Cleaner - parent context is sufficient
google: { offerToken: "..." }

Migration Guide

Old Name (v3.4.3)New Name (v3.4.4)
offerTokenAndroidofferToken
isOfferPersonalizedAndroidisOfferPersonalized
obfuscatedAccountIdAndroidobfuscatedAccountId
obfuscatedProfileIdAndroidobfuscatedProfileId
purchaseTokenAndroidpurchaseToken
replacementModeAndroidreplacementMode
developerBillingOptionAndroiddeveloperBillingOption

Before (v3.4.3):

await requestPurchase({
request: {
google: {
skus: ['product_id'],
offerTokenAndroid: discountOffer.offerTokenAndroid,
isOfferPersonalizedAndroid: true,
obfuscatedAccountIdAndroid: 'user_123',
},
},
type: 'in-app',
});

After (v3.4.4):

await requestPurchase({
request: {
google: {
skus: ['product_id'],
offerToken: discountOffer.offerTokenAndroid, // Note: response field keeps suffix
isOfferPersonalized: true,
obfuscatedAccountId: 'user_123',
},
},
type: 'in-app',
});

Important: Response Types Keep Suffixes

The suffix removal only applies to input types (request parameters). Response types still use suffixes because they're cross-platform:

// Response fields KEEP the Android suffix
const product = products[0] as ProductAndroid;
const discountOffer = product.discountOffers?.[0];

// These response fields still have Android suffix
console.log(discountOffer?.offerTokenAndroid); // ✓ Keep suffix
console.log(discountOffer?.percentageDiscountAndroid); // ✓ Keep suffix

// But input fields don't need it anymore
await requestPurchase({
request: {
google: {
skus: [product.id],
offerToken: discountOffer?.offerTokenAndroid, // Input: no suffix
},
},
type: 'in-app',
});

One-Time Purchase Discount Offers (Android 7.0+)

This release also adds support for discount offers on one-time (in-app) purchases:

import {fetchProducts, requestPurchase} from 'expo-iap';
import type {ProductAndroid} from 'expo-iap';

// 1. Fetch products with discount offers
const products = await fetchProducts({
skus: ['premium_upgrade'],
type: 'in-app',
});

const product = products[0] as ProductAndroid;

// 2. Get the discount offer
const discountOffer = product.discountOffers?.[0];

// 3. Purchase with the discount
if (discountOffer?.offerTokenAndroid) {
await requestPurchase({
request: {
google: {
skus: [product.id],
offerToken: discountOffer.offerTokenAndroid, // Use simplified input field
},
},
type: 'in-app',
});
}

Naming Convention Summary

Field LocationSuffix Required?Example
Inside RequestPurchaseAndroidPropsNOofferToken
Inside RequestSubscriptionAndroidPropsNOpurchaseToken
Cross-platform response typeYESDiscountOffer.offerTokenAndroid

Closes

  • #307 - Support offerToken for in-app products (one-time purchases)

OpenIAP Versions

PackageVersion
openiap-gql1.3.15
openiap-google1.3.26
openiap-apple1.3.13

For detailed changes, see the OpenIAP Release Notes.

v3.4.3 - Win-Back Offers & Advanced Billing Features

· 3 min read
Hyo
Expo IAP Maintainer

This release syncs with OpenIAP v1.3.14, introducing iOS 18+ win-back offers, JWS promotional offers, Android 8.0+ product status codes, and important type cleanup.

New Features

Win-Back Offers (iOS 18+)

Win-back offers allow you to re-engage churned subscribers with special discounts or free trials.

import {requestPurchase} from 'expo-iap';

// Apply a win-back offer during subscription purchase
await requestPurchase({
request: {
apple: {
sku: 'premium_monthly',
winBackOffer: {
offerId: 'winback_50_off', // iOS 18+
},
},
},
type: 'subs',
});

Win-back offers are automatically presented via StoreKit Message when eligible, or can be applied programmatically.

JWS Promotional Offers (iOS 15+)

A new simplified format for promotional offers using compact JWS strings, back-deployed to iOS 15.

await requestPurchase({
request: {
apple: {
sku: 'premium_yearly',
promotionalOfferJWS: {
offerId: 'promo_20_off',
jws: 'eyJhbGciOiJFUzI1NiI...', // Server-signed JWS
},
},
},
type: 'subs',
});

Introductory Offer Eligibility Override (iOS 15+)

Override system-determined introductory offer eligibility.

await requestPurchase({
request: {
apple: {
sku: 'premium_monthly',
introductoryOfferEligibility: true, // Force eligible
},
},
type: 'subs',
});

Product Status Codes (Android 8.0+)

Get detailed feedback on why products couldn't be fetched.

import {fetchProducts} from 'expo-iap';
import type {ProductAndroid} from 'expo-iap';

const result = await fetchProducts({
skus: ['product_1', 'product_2'],
type: 'in-app',
});

result.forEach((product) => {
const androidProduct = product as ProductAndroid;
if (androidProduct.productStatusAndroid) {
switch (androidProduct.productStatusAndroid) {
case 'ok':
// Product available
break;
case 'not-found':
// SKU doesn't exist in Play Console
break;
case 'no-offers-available':
// User not eligible for any offers
break;
}
}
});

Suspended Subscriptions Support (Android 8.1+)

Include suspended subscriptions when fetching available purchases. This feature required native code updates to pass the option through to the OpenIAP SDK.

import {getAvailablePurchases} from 'expo-iap';

const purchases = await getAvailablePurchases({
includeSuspendedAndroid: true, // Include suspended subs
});

// Check if subscription is suspended
purchases.forEach((purchase) => {
if (purchase.isSuspendedAndroid) {
// Direct user to resolve payment issues
// Do NOT grant entitlements for suspended subscriptions
}
});

Important: Suspended subscriptions should NOT be granted entitlements. Users should be directed to the Play Store subscription center to resolve payment issues.

Sub-Response Codes (Android 8.0+)

More granular error information for purchase failures.

// SubResponseCodeAndroid provides additional context:
// - 'no-applicable-sub-response-code'
// - 'payment-declined-due-to-insufficient-funds'
// - 'user-ineligible'

Type Cleanup

Subscription-Only Fields Removed from RequestPurchaseIosProps

The following fields have been removed from RequestPurchaseIosProps because they only apply to subscription purchases:

  • winBackOffer - Win-back offers are subscription-only (iOS 18+)
  • promotionalOfferJWS - JWS promotional offers are subscription-only
  • introductoryOfferEligibility - Introductory eligibility is subscription-only

These fields remain available in RequestSubscriptionIosProps where they belong.

Migration: If you were incorrectly using these fields with one-time purchases, move them to subscription purchases with type: 'subs'.

New Types

TypePlatformDescription
WinBackOfferInputIOSiOS 18+Win-back offer configuration
PromotionalOfferJwsInputIOSiOS 15+JWS promotional offer input
ProductStatusAndroidAndroid 8.0+Product fetch status codes
SubResponseCodeAndroidAndroid 8.0+Granular purchase error codes
BillingResultAndroidAndroid 8.0+Extended billing result with sub-response

Updated Types

  • SubscriptionOfferTypeIOS now includes 'win-back' type
  • RequestSubscriptionIosProps now supports:
    • winBackOffer
    • promotionalOfferJWS
    • introductoryOfferEligibility
    • withOffer (promotional offer)
  • PurchaseOptions now supports includeSuspendedAndroid
  • ProductAndroid and ProductSubscriptionAndroid now include productStatusAndroid

OpenIAP Versions

PackageVersion
openiap-gql1.3.14
openiap-google1.3.25
openiap-apple1.3.13

For detailed changes, see the OpenIAP Release Notes.

3.4.0 - PurchaseState Cleanup & API Consolidation

· One min read
Hyo
Expo IAP Maintainer

This release reflects OpenIAP v1.3.11 updates, simplifying the PurchaseState enum and consolidating the Android billing API.

Breaking Changes

  • PurchaseState: Removed failed, restored, deferred (now only pending, purchased, unknown)
  • AlternativeBillingModeAndroid: Deprecated in favor of BillingProgramAndroid
  • useAlternativeBilling: Deprecated (only logged debug info, had no effect on purchase flow)

Migration Guide

Before (Deprecated)After (Recommended)
alternativeBillingModeAndroid: 'user-choice'enableBillingProgramAndroid: 'user-choice-billing'
alternativeBillingModeAndroid: 'alternative-only'enableBillingProgramAndroid: 'external-offer'
// Before
const {connected} = useIAP({
alternativeBillingModeAndroid: 'user-choice',
});

// After
const {connected} = useIAP({
enableBillingProgramAndroid: 'user-choice-billing',
});

OpenIAP Versions

PackageVersion
openiap-gql1.3.11
openiap-google1.3.21
openiap-apple1.3.9

For detailed changes, see the OpenIAP Release Notes.