Skip to content

Commit 0ad4747

Browse files
authored
feat(google): support one-time purchase discount offers (#51)
BREAKING CHANGE: `oneTimePurchaseOfferDetailsAndroid` type changed from single object to array to support multiple discount offers. Migration: - Before: `product.oneTimePurchaseOfferDetailsAndroid?.formattedPrice` - After: `product.oneTimePurchaseOfferDetailsAndroid?.firstOrNull()?.formattedPrice` Changes: - Add discount-related types: DiscountDisplayInfoAndroid, DiscountAmountAndroid, ValidTimeWindowAndroid, LimitedQuantityInfoAndroid, PreorderDetailsAndroid, RentalDetailsAndroid - Update BillingConverters to use oneTimePurchaseOfferDetailsList API (Google Play Billing Library 7.0+) - Add discount display support in Example app (ProductCard, Modals) - Add discount feature documentation - Fix verifyPurchaseWithIapkit return type usage in Example screens Related issue: hyochan/react-native-iap#3102 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Android: Added discount support for one-time purchases (percentage/amount), multiple offers per product, validity windows, limited-quantity and rental metadata. * UI: Shows original price when discounted, highlights discounted price and displays discount badges and per-offer details. * **Documentation** * New comprehensive "Discounts (Android)" guide with multi-language examples, UI patterns, and best practices. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent c05f2fe commit 0ad4747

17 files changed

Lines changed: 2126 additions & 90 deletions

File tree

packages/apple/Sources/Models/Types.swift

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,26 @@ public struct AppTransaction: Codable {
324324
public var signedDate: Double
325325
}
326326

327+
/// Discount amount details for one-time purchase offers (Android)
328+
/// Available in Google Play Billing Library 7.0+
329+
public struct DiscountAmountAndroid: Codable {
330+
/// Discount amount in micro-units (1,000,000 = 1 unit of currency)
331+
public var discountAmountMicros: String
332+
/// Formatted discount amount with currency sign (e.g., "$4.99")
333+
public var formattedDiscountAmount: String
334+
}
335+
336+
/// Discount display information for one-time purchase offers (Android)
337+
/// Available in Google Play Billing Library 7.0+
338+
public struct DiscountDisplayInfoAndroid: Codable {
339+
/// Absolute discount amount details
340+
/// Only returned for fixed amount discounts
341+
public var discountAmount: DiscountAmountAndroid?
342+
/// Percentage discount (e.g., 33 for 33% off)
343+
/// Only returned for percentage-based discounts
344+
public var percentageDiscount: Int?
345+
}
346+
327347
public struct DiscountIOS: Codable {
328348
public var identifier: String
329349
public var localizedPrice: String?
@@ -376,6 +396,15 @@ public enum FetchProductsResult {
376396
case subscriptions([ProductSubscription]?)
377397
}
378398

399+
/// Limited quantity information for one-time purchase offers (Android)
400+
/// Available in Google Play Billing Library 7.0+
401+
public struct LimitedQuantityInfoAndroid: Codable {
402+
/// Maximum quantity a user can purchase
403+
public var maximumQuantity: Int
404+
/// Remaining quantity the user can still purchase
405+
public var remainingQuantity: Int
406+
}
407+
379408
/// Pre-order details for one-time purchase products (Android)
380409
/// Available in Google Play Billing Library 8.1.0+
381410
public struct PreorderDetailsAndroid: Codable {
@@ -408,21 +437,43 @@ public struct ProductAndroid: Codable, ProductCommon {
408437
public var displayPrice: String
409438
public var id: String
410439
public var nameAndroid: String
411-
public var oneTimePurchaseOfferDetailsAndroid: ProductAndroidOneTimePurchaseOfferDetail?
440+
/// One-time purchase offer details including discounts (Android)
441+
/// Returns all eligible offers. Available in Google Play Billing Library 7.0+
442+
public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]?
412443
public var platform: IapPlatform = .android
413444
public var price: Double?
414445
public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]?
415446
public var title: String
416447
public var type: ProductType = .inApp
417448
}
418449

450+
/// One-time purchase offer details (Android)
451+
/// Available in Google Play Billing Library 7.0+
419452
public struct ProductAndroidOneTimePurchaseOfferDetail: Codable {
453+
/// Discount display information
454+
/// Only available for discounted offers
455+
public var discountDisplayInfo: DiscountDisplayInfoAndroid?
420456
public var formattedPrice: String
421-
/// Pre-order details for products available for pre-order (Android)
457+
/// Full (non-discounted) price in micro-units
458+
/// Only available for discounted offers
459+
public var fullPriceMicros: String?
460+
/// Limited quantity information
461+
public var limitedQuantityInfo: LimitedQuantityInfoAndroid?
462+
/// Offer ID
463+
public var offerId: String?
464+
/// List of offer tags
465+
public var offerTags: [String]
466+
/// Offer token for use in BillingFlowParams when purchasing
467+
public var offerToken: String
468+
/// Pre-order details for products available for pre-order
422469
/// Available in Google Play Billing Library 8.1.0+
423470
public var preorderDetailsAndroid: PreorderDetailsAndroid?
424471
public var priceAmountMicros: String
425472
public var priceCurrencyCode: String
473+
/// Rental details for rental offers
474+
public var rentalDetailsAndroid: RentalDetailsAndroid?
475+
/// Valid time window for the offer
476+
public var validTimeWindow: ValidTimeWindowAndroid?
426477
}
427478

428479
public struct ProductIOS: Codable, ProductCommon {
@@ -451,7 +502,9 @@ public struct ProductSubscriptionAndroid: Codable, ProductCommon {
451502
public var displayPrice: String
452503
public var id: String
453504
public var nameAndroid: String
454-
public var oneTimePurchaseOfferDetailsAndroid: ProductAndroidOneTimePurchaseOfferDetail?
505+
/// One-time purchase offer details including discounts (Android)
506+
/// Returns all eligible offers. Available in Google Play Billing Library 7.0+
507+
public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]?
455508
public var platform: IapPlatform = .android
456509
public var price: Double?
457510
public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]
@@ -609,6 +662,16 @@ public struct RenewalInfoIOS: Codable {
609662
public var willAutoRenew: Bool
610663
}
611664

665+
/// Rental details for one-time purchase products that can be rented (Android)
666+
/// Available in Google Play Billing Library 7.0+
667+
public struct RentalDetailsAndroid: Codable {
668+
/// Rental expiration period in ISO 8601 format
669+
/// Time after rental period ends when user can still extend
670+
public var rentalExpirationPeriod: String?
671+
/// Rental period in ISO 8601 format (e.g., P7D for 7 days)
672+
public var rentalPeriod: String
673+
}
674+
612675
public enum RequestPurchaseResult {
613676
case purchase(Purchase?)
614677
case purchases([Purchase]?)
@@ -658,6 +721,15 @@ public struct UserChoiceBillingDetails: Codable {
658721
public var products: [String]
659722
}
660723

724+
/// Valid time window for when an offer is available (Android)
725+
/// Available in Google Play Billing Library 7.0+
726+
public struct ValidTimeWindowAndroid: Codable {
727+
/// End time in milliseconds since epoch
728+
public var endTimeMillis: String
729+
/// Start time in milliseconds since epoch
730+
public var startTimeMillis: String
731+
}
732+
661733
public struct VerifyPurchaseResultAndroid: Codable {
662734
public var autoRenewing: Bool
663735
public var betaProduct: Bool

packages/docs/src/pages/docs.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Events from './docs/events';
1010
import Errors from './docs/errors';
1111
import Purchase from './docs/features/purchase';
1212
import SubscriptionFeature from './docs/features/subscription';
13+
import Discount from './docs/features/discount';
1314
import OfferCodeRedemption from './docs/features/offer-code-redemption';
1415
import ExternalPurchase from './docs/features/external-purchase';
1516
import SubscriptionUpgradeDowngrade from './docs/features/subscription-upgrade-downgrade';
@@ -170,6 +171,15 @@ function Docs() {
170171
Subscription
171172
</NavLink>
172173
</li>
174+
<li>
175+
<NavLink
176+
to="/docs/features/discount"
177+
className={({ isActive }) => (isActive ? 'active' : '')}
178+
onClick={closeSidebar}
179+
>
180+
Discounts (Android)
181+
</NavLink>
182+
</li>
173183
<li>
174184
<NavLink
175185
to="/docs/features/offer-code-redemption"
@@ -245,6 +255,7 @@ function Docs() {
245255
path="features/subscription"
246256
element={<SubscriptionFeature />}
247257
/>
258+
<Route path="features/discount" element={<Discount />} />
248259
<Route
249260
path="features/offer-code-redemption"
250261
element={<OfferCodeRedemption />}

0 commit comments

Comments
 (0)