Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"inapp",
"netrc",
"openiap",
"preorder",
"skus",
"swiftpm",
"tvos",
Expand Down
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ func requestPurchaseIOS() // Should be requestPurchase() if cross-platform

- **Sources/Models/**: OpenIAP official types that match [openiap.dev/docs/types](https://www.openiap.dev/docs/types)

- `Product.swift` - OpenIapProduct and related types
- `Purchase.swift` - OpenIapPurchase and related types
- `Product.swift` - ProductIOS and related types
- `Purchase.swift` - PurchaseIOS and related types
- `ActiveSubscription.swift` - ActiveSubscription type
- `PurchaseError.swift` - PurchaseError type
- `Receipt.swift` - Receipt validation types
Expand All @@ -122,7 +122,6 @@ func requestPurchaseIOS() // Should be requestPurchase() if cross-platform
- `OpenIapModule.swift` - Core implementation
- `OpenIapStore.swift` - SwiftUI-friendly store
- `OpenIapProtocol.swift` - API interface definitions
- `OpenIapError.swift` - Error definitions

### Naming Rules

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ swift test
#### OpenIap Prefix (Public Models)

- Prefix all public model types with `OpenIap`.
- Examples: `OpenIapProduct`, `OpenIapPurchase`, `OpenIapProductRequest`, `OpenIapRequestPurchaseProps`, `OpenIapPurchaseOptions`, `OpenIapReceiptValidationProps`, `OpenIapReceiptValidationResult`, `OpenIapActiveSubscription`, `OpenIapPurchaseState`, `OpenIapPurchaseOffer`, `OpenIapProductType`, `OpenIapProductTypeIOS`.
- Examples: `ProductIOS`, `PurchaseIOS`, `ProductIOSRequest`, `RequestPurchaseProps`, `PurchaseIOSOptions`, `ReceiptValidationProps`, `ReceiptValidationResultIOS`, `ActiveSubscription`, `PurchaseIOSState`, `PurchaseIOSOffer`, `ProductIOSType`, `ProductIOSTypeIOS`.
- Private/internal helper types do not need the prefix.
- When renaming existing types, add a public `typealias` from the old name to the new name to preserve source compatibility, then migrate usages incrementally.

Expand Down
16 changes: 13 additions & 3 deletions Example/Martie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
C0E1F6002C8F1ABA00123456 /* OfferCodeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E1F5FF2C8F1ABA00123456 /* OfferCodeScreen.swift */; };
C0E1F6042C8F1AC500123456 /* AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E1F6032C8F1AC500123456 /* AppColors.swift */; };
C0E1F6072C8F1AD000123456 /* OpenIAP in Frameworks */ = {isa = PBXBuildFile; productRef = C0E1F6062C8F1AD000123456 /* OpenIAP */; };
C0IAC0012D10000000000001 /* IapCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0IAC0002D10000000000001 /* IapCompat.swift */; };
C0UI00012D00000000000001 /* FeatureCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10012D00000000000001 /* FeatureCard.swift */; };
C0UI00022D00000000000002 /* LoadingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10022D00000000000002 /* LoadingCard.swift */; };
C0UI00032D00000000000003 /* EmptyStateCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10032D00000000000003 /* EmptyStateCard.swift */; };
Expand All @@ -33,6 +34,7 @@
C0UI000D2D0000000000000D /* TestingNotesCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100D2D0000000000000D /* TestingNotesCard.swift */; };
C0UI000E2D0000000000000E /* TestingNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100E2D0000000000000E /* TestingNote.swift */; };
C0UI000F2D0000000000000F /* PurchaseCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100F2D0000000000000F /* PurchaseCard.swift */; };
C0UI00102D00000000000010 /* PurchaseDetailSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10102D00000000000010 /* PurchaseDetailSheet.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -47,6 +49,7 @@
C0E1F5FD2C8F1AB500123456 /* AvailablePurchasesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailablePurchasesScreen.swift; sourceTree = "<group>"; };
C0E1F5FF2C8F1ABA00123456 /* OfferCodeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferCodeScreen.swift; sourceTree = "<group>"; };
C0E1F6032C8F1AC500123456 /* AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColors.swift; sourceTree = "<group>"; };
C0IAC0002D10000000000001 /* IapCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IapCompat.swift; sourceTree = "<group>"; };
C0UI10012D00000000000001 /* FeatureCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureCard.swift; sourceTree = "<group>"; };
C0UI10022D00000000000002 /* LoadingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCard.swift; sourceTree = "<group>"; };
C0UI10032D00000000000003 /* EmptyStateCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateCard.swift; sourceTree = "<group>"; };
Expand All @@ -62,6 +65,7 @@
C0UI100D2D0000000000000D /* TestingNotesCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingNotesCard.swift; sourceTree = "<group>"; };
C0UI100E2D0000000000000E /* TestingNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingNote.swift; sourceTree = "<group>"; };
C0UI100F2D0000000000000F /* PurchaseCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCard.swift; sourceTree = "<group>"; };
C0UI10102D00000000000010 /* PurchaseDetailSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDetailSheet.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -138,6 +142,7 @@
isa = PBXGroup;
children = (
C0E1F6032C8F1AC500123456 /* AppColors.swift */,
C0IAC0002D10000000000001 /* IapCompat.swift */,
);
path = Models;
sourceTree = "<group>";
Expand All @@ -160,6 +165,7 @@
C0UI100D2D0000000000000D /* TestingNotesCard.swift */,
C0UI100E2D0000000000000E /* TestingNote.swift */,
C0UI100F2D0000000000000F /* PurchaseCard.swift */,
C0UI10102D00000000000010 /* PurchaseDetailSheet.swift */,
);
path = uis;
sourceTree = "<group>";
Expand Down Expand Up @@ -195,7 +201,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1500;
LastUpgradeCheck = 1500;
LastUpgradeCheck = 2600;
TargetAttributes = {
C0E1F5E42C8F1A9400123456 = {
CreatedOnToolsVersion = 15.0;
Expand Down Expand Up @@ -247,6 +253,7 @@
C0E1F5FE2C8F1AB500123456 /* AvailablePurchasesScreen.swift in Sources */,
C0E1F6002C8F1ABA00123456 /* OfferCodeScreen.swift in Sources */,
C0E1F6042C8F1AC500123456 /* AppColors.swift in Sources */,
C0IAC0012D10000000000001 /* IapCompat.swift in Sources */,
C0E1F5E92C8F1A9400123456 /* OpenIapExampleApp.swift in Sources */,
C0UI00012D00000000000001 /* FeatureCard.swift in Sources */,
C0UI00022D00000000000002 /* LoadingCard.swift in Sources */,
Expand All @@ -263,6 +270,7 @@
C0UI000D2D0000000000000D /* TestingNotesCard.swift in Sources */,
C0UI000E2D0000000000000E /* TestingNote.swift in Sources */,
C0UI000F2D0000000000000F /* PurchaseCard.swift in Sources */,
C0UI00102D00000000000010 /* PurchaseDetailSheet.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -304,6 +312,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = PRDQGB267K;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -327,6 +336,7 @@
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
Expand Down Expand Up @@ -367,6 +377,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = PRDQGB267K;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -383,6 +394,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
Expand All @@ -396,7 +408,6 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"OpenIapExample/Preview Content\"";
DEVELOPMENT_TEAM = PRDQGB267K;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = OpenIAP;
Expand Down Expand Up @@ -427,7 +438,6 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"OpenIapExample/Preview Content\"";
DEVELOPMENT_TEAM = PRDQGB267K;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = OpenIAP;
Expand Down
55 changes: 55 additions & 0 deletions Example/OpenIapExample/Models/IapCompat.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import OpenIAP

// Maintain previous sample code naming expectations while using generated models
@available(iOS 15.0, *)
typealias OpenIapProduct = ProductIOS
@available(iOS 15.0, *)
typealias OpenIapPurchase = PurchaseIOS
@available(iOS 15.0, *)
typealias OpenIapError = PurchaseError
@available(iOS 15.0, *)
typealias OpenIapActiveSubscription = ActiveSubscription

@available(iOS 15.0, *)
extension PurchaseState {
var isAcknowledged: Bool {
switch self {
case .purchased, .restored:
return true
default:
return false
}
}
}

@available(iOS 15.0, *)
extension PurchaseIOS {
var isSubscription: Bool {
expirationDateIOS != nil || isAutoRenewing
}
}

@available(iOS 15.0, *)
extension ProductIOS {
var productIdentifier: String { id }
}

@available(iOS 15.0, *)
extension OpenIAP.Product {
func asIOS() -> OpenIapProduct? {
if case let .productIos(value) = self {
return value
}
return nil
}
}

@available(iOS 15.0, *)
extension OpenIAP.Purchase {
func asIOS() -> OpenIapPurchase? {
if case let .purchaseIos(value) = self {
return value
}
return nil
}
}
60 changes: 35 additions & 25 deletions Example/OpenIapExample/Screens/AvailablePurchasesScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct AvailablePurchasesScreen: View {
@StateObject private var iapStore = OpenIapStore()
@State private var showError = false
@State private var errorMessage = ""
@State private var selectedPurchase: OpenIapPurchase?

var body: some View {
ScrollView {
Expand Down Expand Up @@ -33,6 +34,14 @@ struct AvailablePurchasesScreen: View {
.disabled(iapStore.status.loadings.restorePurchases)
}
}
.sheet(isPresented: Binding(
get: { selectedPurchase != nil },
set: { if !$0 { selectedPurchase = nil } }
)) {
if let purchase = selectedPurchase {
PurchaseDetailSheet(purchase: purchase)
}
}
.alert("Error", isPresented: $showError) {
Button("OK") {}
} message: {
Expand Down Expand Up @@ -60,15 +69,11 @@ struct AvailablePurchasesScreen: View {
}

private var uniqueActivePurchases: [OpenIapPurchase] {
let allActivePurchases = iapStore.availablePurchases.filter { purchase in
let allActivePurchases = iapStore.iosAvailablePurchases.filter { purchase in
// Show active purchases (purchased or restored state)
purchase.purchaseState == .purchased || purchase.purchaseState == .restored
}.filter { purchase in
// Check product type if we can determine it
let isSubscription = purchase.productId.contains("premium") ||
purchase.productId.contains("subscription")

if isSubscription {
if purchase.isSubscription {
// Active subscriptions: check auto-renewing or expiry time
if purchase.isAutoRenewing {
return true // Always show auto-renewing subscriptions
Expand Down Expand Up @@ -99,12 +104,14 @@ struct AvailablePurchasesScreen: View {
)
} else {
VStack(spacing: 12) {
ForEach(uniqueActivePurchases, id: \.id) { purchase in
ActivePurchaseCard(purchase: purchase) {
ForEach(uniqueActivePurchases, id: \.transactionId) { purchase in
ActivePurchaseCard(purchase: purchase, onConsume: {
Task {
await finishPurchase(purchase)
}
}
}, onShowDetails: {
selectedPurchase = purchase
})
}
}
.padding(.horizontal)
Expand All @@ -126,16 +133,18 @@ struct AvailablePurchasesScreen: View {

@ViewBuilder
private var purchaseHistoryContent: some View {
if iapStore.availablePurchases.isEmpty {
if iapStore.iosAvailablePurchases.isEmpty {
EmptyStateCard(
icon: "clock",
title: "No purchase history",
subtitle: "Your purchase history will appear here"
)
} else {
VStack(spacing: 12) {
ForEach(iapStore.availablePurchases.sorted { $0.transactionDate > $1.transactionDate }, id: \.id) { purchase in
PurchaseHistoryCard(purchase: purchase)
ForEach(iapStore.iosAvailablePurchases.sorted { $0.transactionDate > $1.transactionDate }, id: \.transactionId) { purchase in
PurchaseHistoryCard(purchase: purchase) {
selectedPurchase = purchase
}
}
}
.padding(.horizontal)
Expand Down Expand Up @@ -277,9 +286,10 @@ struct AvailablePurchasesScreen: View {
print("🔷 [AvailablePurchases] Setting up OpenIapStore...")

iapStore.onPurchaseSuccess = { purchase in
Task { @MainActor in
// Refresh purchases when new purchase comes in
loadPurchases()
if purchase.asIOS() != nil {
Task { @MainActor in
loadPurchases()
}
}
}

Expand Down Expand Up @@ -318,7 +328,7 @@ struct AvailablePurchasesScreen: View {
Task {
do {
try await iapStore.getAvailablePurchases()
print("✅ [AvailablePurchases] Loaded \(iapStore.availablePurchases.count) purchases")
print("✅ [AvailablePurchases] Loaded \(iapStore.iosAvailablePurchases.count) purchases")
} catch {
await MainActor.run {
errorMessage = "Failed to load purchases: \(error.localizedDescription)"
Expand All @@ -332,10 +342,10 @@ struct AvailablePurchasesScreen: View {

private func finishPurchase(_ purchase: OpenIapPurchase) async {
do {
_ = try await iapStore.finishTransaction(purchase: purchase)
try await iapStore.finishTransaction(purchase: purchase)
print("✅ [AvailablePurchases] Transaction finished: \(purchase.id)")
// Reload purchases to update UI
await loadPurchases()
loadPurchases()
} catch {
await MainActor.run {
errorMessage = "Failed to finish transaction: \(error.localizedDescription)"
Expand All @@ -349,30 +359,30 @@ struct AvailablePurchasesScreen: View {
private func clearAllTransactions() async {
// Note: This would require additional API in OpenIapStore
// For now, just reload purchases
await loadPurchases()
loadPurchases()
print("🧪 [AvailablePurchases] Clear transactions requested (reloaded purchases)")
}

private func syncSubscriptions() async {
// Reload purchases to sync subscription status
await loadPurchases()
loadPurchases()
print("🧪 [AvailablePurchases] Subscription sync requested (reloaded purchases)")
}

private func finishUnfinishedTransactions() async {
let unfinishedPurchases = iapStore.availablePurchases.filter { !$0.purchaseState.isAcknowledged }
let unfinishedPurchases = iapStore.iosAvailablePurchases.filter { !$0.purchaseState.isAcknowledged }

for purchase in unfinishedPurchases {
do {
_ = try await iapStore.finishTransaction(purchase: purchase)
print("✅ [AvailablePurchases] Finished unfinished transaction: \(purchase.id)")
try await iapStore.finishTransaction(purchase: purchase)
print("✅ [AvailablePurchases] Finished unfinished transaction: \(purchase.transactionId)")
} catch {
print("❌ [AvailablePurchases] Failed to finish transaction \(purchase.id): \(error)")
print("❌ [AvailablePurchases] Failed to finish transaction \(purchase.transactionId): \(error)")
}
}

// Reload after finishing transactions
await loadPurchases()
loadPurchases()
}
}

Expand Down
6 changes: 4 additions & 2 deletions Example/OpenIapExample/Screens/OfferCodeScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ struct OfferCodeScreen: View {
print("🔷 [OfferCode] Setting up OpenIapStore...")

iapStore.onPurchaseSuccess = { purchase in
Task { @MainActor in
print("✅ [OfferCode] Offer code redeemed successfully: \(purchase.productId)")
if let iosPurchase = purchase.asIOS() {
Task { @MainActor in
print("✅ [OfferCode] Offer code redeemed successfully: \(iosPurchase.productId)")
}
}
}

Expand Down
Loading