Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.

Commit 9dac6fe

Browse files
authored
UI refactor and store improvements (#4)
This series of commits refactors the OpenIAP module by introducing structured concurrency and standardizing public models. `IapState` and `ProductManager` are now `actors`, with events and listeners marked `@sendable` and UIKit access restricted to `MainActor`. Public APIs adopt consistent `OpenIap*` naming, with typealiases provided for backward compatibility, while serialization is consolidated under `OpenIapSerialization` for safer bridging and analytics. The `restorePurchases()` method has been removed in favor of `refreshPurchases(forceSync:)`, and error handling is centralized in `OpenIapError`. Supporting types like `IapStatus` are nested under `OpenIapStore` for a cleaner structure. These changes remove outdated availability checks and legacy fallbacks, aligning the codebase with the declared baselines (iOS 15+/tvOS 15+/macOS 14+), reducing maintenance overhead, and ensuring more predictable behavior without altering most external APIs.
1 parent 09ca64b commit 9dac6fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+3574
-2225
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"cSpell.words": [
33
"hyodotdev",
4+
"inapp",
45
"netrc",
56
"openiap",
67
"skus",

CLAUDE.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
# Claude Development Guidelines for OpenIAP
22

3+
## Function Naming Conventions
4+
5+
### Platform-Specific Functions
6+
7+
- **iOS-specific functions MUST have `IOS` suffix**
8+
- **Android-specific functions MUST have `Android` suffix**
9+
- **Cross-platform functions have NO suffix**
10+
11+
#### Examples
12+
13+
##### ✅ Correct
14+
```swift
15+
// iOS-specific functions
16+
func presentCodeRedemptionSheetIOS()
17+
func showManageSubscriptionsIOS()
18+
func deepLinkToSubscriptionsIOS()
19+
func getPromotedProductIOS()
20+
func requestPurchaseOnPromotedProductIOS()
21+
func syncIOS()
22+
func getReceiptDataIOS()
23+
24+
// Cross-platform functions
25+
func initConnection()
26+
func fetchProducts()
27+
func requestPurchase()
28+
func finishTransaction()
29+
```
30+
31+
##### ❌ Incorrect
32+
```swift
33+
// Missing IOS suffix for iOS-specific
34+
func presentCodeRedemptionSheet() // Should be presentCodeRedemptionSheetIOS()
35+
func showManageSubscriptions() // Should be showManageSubscriptionsIOS()
36+
37+
// Unnecessary suffix for cross-platform
38+
func requestPurchaseIOS() // Should be requestPurchase() if cross-platform
39+
```
40+
41+
### API Naming Alignment
42+
43+
- **MUST match openiap.dev API naming**
44+
- **Use exact same function names as React Native OpenIAP**
45+
46+
#### Standard API Names
47+
- `initConnection()` - Initialize IAP connection
48+
- `endConnection()` - End IAP connection
49+
- `fetchProducts()` - Fetch products from store
50+
- `getProducts()` - Get cached products
51+
- `getAvailablePurchases()` - Get available/restored purchases
52+
- `requestPurchase()` - Request a purchase
53+
- `finishTransaction()` - Finish a transaction
54+
- `restorePurchases()` - Restore previous purchases
55+
356
## Swift Naming Conventions for Acronyms
457

558
### General Rule
@@ -41,6 +94,34 @@
4194
- Build with: `swift build`
4295
- Use real product IDs: `dev.hyo.martie.10bulbs`, `dev.hyo.martie.30bulbs`
4396

97+
## File Organization
98+
99+
### Directory Structure
100+
101+
- **Sources/Models/**: OpenIAP official types that match [openiap.dev/docs/types](https://www.openiap.dev/docs/types)
102+
- `Product.swift` - OpenIapProduct and related types
103+
- `Purchase.swift` - OpenIapPurchase and related types
104+
- `ActiveSubscription.swift` - ActiveSubscription type
105+
- `PurchaseError.swift` - PurchaseError type
106+
- `Receipt.swift` - Receipt validation types
107+
- etc.
108+
109+
- **Sources/Helpers/**: Internal helper classes (NOT in OpenIAP official types)
110+
- `ProductManager.swift` - Thread-safe product caching
111+
- `IapStatus.swift` - UI status management for SwiftUI
112+
113+
- **Sources/**: Main module files
114+
- `OpenIapModule.swift` - Core implementation
115+
- `OpenIapStore.swift` - SwiftUI-friendly store
116+
- `OpenIapProtocol.swift` - API interface definitions
117+
- `OpenIapError.swift` - Error definitions
118+
119+
### Naming Rules
120+
121+
- **Models**: Must match OpenIAP specification exactly
122+
- **Helpers**: Use descriptive names ending with purpose (Manager, Cache, Status, etc.)
123+
- **Avoid confusing names**: Don't use "Store" for caching classes (use Cache, Manager instead)
124+
44125
## Development Notes
45126

46127
- Purchase Flow should display real-time purchase events

CONTRIBUTING.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ swift test
3939
- **Acronyms as suffix**: Use all caps (`ProductIAP`, `ManagerIOS`)
4040
- See [CLAUDE.md](CLAUDE.md) for detailed naming rules
4141

42+
#### OpenIap Prefix (Public Models)
43+
44+
- Prefix all public model types with `OpenIap`.
45+
- Examples: `OpenIapProduct`, `OpenIapPurchase`, `OpenIapProductRequest`, `OpenIapRequestPurchaseProps`, `OpenIapPurchaseOptions`, `OpenIapReceiptValidationProps`, `OpenIapReceiptValidationResult`, `OpenIapActiveSubscription`, `OpenIapPurchaseState`, `OpenIapPurchaseOffer`, `OpenIapProductType`, `OpenIapProductTypeIOS`.
46+
- Private/internal helper types do not need the prefix.
47+
- When renaming existing types, add a public `typealias` from the old name to the new name to preserve source compatibility, then migrate usages incrementally.
48+
4249
## Testing
4350

4451
All new features must include tests:
@@ -111,4 +118,4 @@ Feel free to:
111118

112119
## License
113120

114-
By contributing, you agree that your contributions will be licensed under the MIT License.
121+
By contributing, you agree that your contributions will be licensed under the MIT License.

Example/Martie.xcodeproj/project.pbxproj

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,23 @@
1616
C0E1F5FC2C8F1AB000123456 /* SubscriptionFlowScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E1F5FB2C8F1AB000123456 /* SubscriptionFlowScreen.swift */; };
1717
C0E1F5FE2C8F1AB500123456 /* AvailablePurchasesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E1F5FD2C8F1AB500123456 /* AvailablePurchasesScreen.swift */; };
1818
C0E1F6002C8F1ABA00123456 /* OfferCodeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E1F5FF2C8F1ABA00123456 /* OfferCodeScreen.swift */; };
19-
C0E1F6022C8F1AC000123456 /* StoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E1F6012C8F1AC000123456 /* StoreViewModel.swift */; };
2019
C0E1F6042C8F1AC500123456 /* AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E1F6032C8F1AC500123456 /* AppColors.swift */; };
2120
C0E1F6072C8F1AD000123456 /* OpenIAP in Frameworks */ = {isa = PBXBuildFile; productRef = C0E1F6062C8F1AD000123456 /* OpenIAP */; };
21+
C0UI00012D00000000000001 /* FeatureCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10012D00000000000001 /* FeatureCard.swift */; };
22+
C0UI00022D00000000000002 /* LoadingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10022D00000000000002 /* LoadingCard.swift */; };
23+
C0UI00032D00000000000003 /* EmptyStateCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10032D00000000000003 /* EmptyStateCard.swift */; };
24+
C0UI00042D00000000000004 /* SectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10042D00000000000004 /* SectionHeaderView.swift */; };
25+
C0UI00052D00000000000005 /* ProductListCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10052D00000000000005 /* ProductListCard.swift */; };
26+
C0UI00062D00000000000006 /* ProductGridCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10062D00000000000006 /* ProductGridCard.swift */; };
27+
C0UI00072D00000000000007 /* ActivePurchaseCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10072D00000000000007 /* ActivePurchaseCard.swift */; };
28+
C0UI00082D00000000000008 /* InstructionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10082D00000000000008 /* InstructionRow.swift */; };
29+
C0UI00092D00000000000009 /* ProductCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10092D00000000000009 /* ProductCard.swift */; };
30+
C0UI000A2D0000000000000A /* SubscriptionCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100A2D0000000000000A /* SubscriptionCard.swift */; };
31+
C0UI000B2D0000000000000B /* PurchaseHistoryCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100B2D0000000000000B /* PurchaseHistoryCard.swift */; };
32+
C0UI000C2D0000000000000C /* InstructionCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100C2D0000000000000C /* InstructionCard.swift */; };
33+
C0UI000D2D0000000000000D /* TestingNotesCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100D2D0000000000000D /* TestingNotesCard.swift */; };
34+
C0UI000E2D0000000000000E /* TestingNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100E2D0000000000000E /* TestingNote.swift */; };
35+
C0UI000F2D0000000000000F /* PurchaseCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100F2D0000000000000F /* PurchaseCard.swift */; };
2236
/* End PBXBuildFile section */
2337

2438
/* Begin PBXFileReference section */
@@ -32,8 +46,22 @@
3246
C0E1F5FB2C8F1AB000123456 /* SubscriptionFlowScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowScreen.swift; sourceTree = "<group>"; };
3347
C0E1F5FD2C8F1AB500123456 /* AvailablePurchasesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailablePurchasesScreen.swift; sourceTree = "<group>"; };
3448
C0E1F5FF2C8F1ABA00123456 /* OfferCodeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferCodeScreen.swift; sourceTree = "<group>"; };
35-
C0E1F6012C8F1AC000123456 /* StoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreViewModel.swift; sourceTree = "<group>"; };
3649
C0E1F6032C8F1AC500123456 /* AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColors.swift; sourceTree = "<group>"; };
50+
C0UI10012D00000000000001 /* FeatureCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureCard.swift; sourceTree = "<group>"; };
51+
C0UI10022D00000000000002 /* LoadingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCard.swift; sourceTree = "<group>"; };
52+
C0UI10032D00000000000003 /* EmptyStateCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateCard.swift; sourceTree = "<group>"; };
53+
C0UI10042D00000000000004 /* SectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderView.swift; sourceTree = "<group>"; };
54+
C0UI10052D00000000000005 /* ProductListCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListCard.swift; sourceTree = "<group>"; };
55+
C0UI10062D00000000000006 /* ProductGridCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductGridCard.swift; sourceTree = "<group>"; };
56+
C0UI10072D00000000000007 /* ActivePurchaseCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePurchaseCard.swift; sourceTree = "<group>"; };
57+
C0UI10082D00000000000008 /* InstructionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionRow.swift; sourceTree = "<group>"; };
58+
C0UI10092D00000000000009 /* ProductCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCard.swift; sourceTree = "<group>"; };
59+
C0UI100A2D0000000000000A /* SubscriptionCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionCard.swift; sourceTree = "<group>"; };
60+
C0UI100B2D0000000000000B /* PurchaseHistoryCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseHistoryCard.swift; sourceTree = "<group>"; };
61+
C0UI100C2D0000000000000C /* InstructionCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionCard.swift; sourceTree = "<group>"; };
62+
C0UI100D2D0000000000000D /* TestingNotesCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingNotesCard.swift; sourceTree = "<group>"; };
63+
C0UI100E2D0000000000000E /* TestingNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingNote.swift; sourceTree = "<group>"; };
64+
C0UI100F2D0000000000000F /* PurchaseCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCard.swift; sourceTree = "<group>"; };
3765
/* End PBXFileReference section */
3866

3967
/* Begin PBXFrameworksBuildPhase section */
@@ -67,9 +95,9 @@
6795
C0E1F5E72C8F1A9400123456 /* OpenIapExample */ = {
6896
isa = PBXGroup;
6997
children = (
70-
C0E1F6082C8F1AD500123456 /* Screens */,
7198
C0E1F6092C8F1ADB00123456 /* ViewModels */,
7299
C0E1F60A2C8F1AE000123456 /* Models */,
100+
C0E1F6082C8F1AD500123456 /* Screens */,
73101
C0E1F5E82C8F1A9400123456 /* OpenIapExampleApp.swift */,
74102
C0E1F5EA2C8F1A9400123456 /* ContentView.swift */,
75103
C0E1F5EC2C8F1A9500123456 /* Assets.xcassets */,
@@ -89,6 +117,7 @@
89117
C0E1F6082C8F1AD500123456 /* Screens */ = {
90118
isa = PBXGroup;
91119
children = (
120+
C0UI20012D00000000000001 /* uis */,
92121
C0E1F5F72C8F1AA300123456 /* HomeScreen.swift */,
93122
C0E1F5F92C8F1AAB00123456 /* PurchaseFlowScreen.swift */,
94123
C0E1F5FB2C8F1AB000123456 /* SubscriptionFlowScreen.swift */,
@@ -101,7 +130,6 @@
101130
C0E1F6092C8F1ADB00123456 /* ViewModels */ = {
102131
isa = PBXGroup;
103132
children = (
104-
C0E1F6012C8F1AC000123456 /* StoreViewModel.swift */,
105133
);
106134
path = ViewModels;
107135
sourceTree = "<group>";
@@ -114,6 +142,28 @@
114142
path = Models;
115143
sourceTree = "<group>";
116144
};
145+
C0UI20012D00000000000001 /* uis */ = {
146+
isa = PBXGroup;
147+
children = (
148+
C0UI10012D00000000000001 /* FeatureCard.swift */,
149+
C0UI10022D00000000000002 /* LoadingCard.swift */,
150+
C0UI10032D00000000000003 /* EmptyStateCard.swift */,
151+
C0UI10042D00000000000004 /* SectionHeaderView.swift */,
152+
C0UI10052D00000000000005 /* ProductListCard.swift */,
153+
C0UI10062D00000000000006 /* ProductGridCard.swift */,
154+
C0UI10072D00000000000007 /* ActivePurchaseCard.swift */,
155+
C0UI10082D00000000000008 /* InstructionRow.swift */,
156+
C0UI10092D00000000000009 /* ProductCard.swift */,
157+
C0UI100A2D0000000000000A /* SubscriptionCard.swift */,
158+
C0UI100B2D0000000000000B /* PurchaseHistoryCard.swift */,
159+
C0UI100C2D0000000000000C /* InstructionCard.swift */,
160+
C0UI100D2D0000000000000D /* TestingNotesCard.swift */,
161+
C0UI100E2D0000000000000E /* TestingNote.swift */,
162+
C0UI100F2D0000000000000F /* PurchaseCard.swift */,
163+
);
164+
path = uis;
165+
sourceTree = "<group>";
166+
};
117167
/* End PBXGroup section */
118168

119169
/* Begin PBXNativeTarget section */
@@ -152,7 +202,7 @@
152202
};
153203
};
154204
};
155-
buildConfigurationList = C0E1F5E02C8F1A9400123456 /* Build configuration list for PBXProject "OpenIapExample" */;
205+
buildConfigurationList = C0E1F5E02C8F1A9400123456 /* Build configuration list for PBXProject "Martie" */;
156206
compatibilityVersion = "Xcode 14.0";
157207
developmentRegion = en;
158208
hasScannedForEncodings = 0;
@@ -196,9 +246,23 @@
196246
C0E1F5FC2C8F1AB000123456 /* SubscriptionFlowScreen.swift in Sources */,
197247
C0E1F5FE2C8F1AB500123456 /* AvailablePurchasesScreen.swift in Sources */,
198248
C0E1F6002C8F1ABA00123456 /* OfferCodeScreen.swift in Sources */,
199-
C0E1F6022C8F1AC000123456 /* StoreViewModel.swift in Sources */,
200249
C0E1F6042C8F1AC500123456 /* AppColors.swift in Sources */,
201250
C0E1F5E92C8F1A9400123456 /* OpenIapExampleApp.swift in Sources */,
251+
C0UI00012D00000000000001 /* FeatureCard.swift in Sources */,
252+
C0UI00022D00000000000002 /* LoadingCard.swift in Sources */,
253+
C0UI00032D00000000000003 /* EmptyStateCard.swift in Sources */,
254+
C0UI00042D00000000000004 /* SectionHeaderView.swift in Sources */,
255+
C0UI00052D00000000000005 /* ProductListCard.swift in Sources */,
256+
C0UI00062D00000000000006 /* ProductGridCard.swift in Sources */,
257+
C0UI00072D00000000000007 /* ActivePurchaseCard.swift in Sources */,
258+
C0UI00082D00000000000008 /* InstructionRow.swift in Sources */,
259+
C0UI00092D00000000000009 /* ProductCard.swift in Sources */,
260+
C0UI000A2D0000000000000A /* SubscriptionCard.swift in Sources */,
261+
C0UI000B2D0000000000000B /* PurchaseHistoryCard.swift in Sources */,
262+
C0UI000C2D0000000000000C /* InstructionCard.swift in Sources */,
263+
C0UI000D2D0000000000000D /* TestingNotesCard.swift in Sources */,
264+
C0UI000E2D0000000000000E /* TestingNote.swift in Sources */,
265+
C0UI000F2D0000000000000F /* PurchaseCard.swift in Sources */,
202266
);
203267
runOnlyForDeploymentPostprocessing = 0;
204268
};
@@ -335,7 +399,7 @@
335399
DEVELOPMENT_TEAM = PRDQGB267K;
336400
ENABLE_PREVIEWS = YES;
337401
GENERATE_INFOPLIST_FILE = YES;
338-
INFOPLIST_KEY_CFBundleDisplayName = "OpenIAP Example";
402+
INFOPLIST_KEY_CFBundleDisplayName = OpenIAP;
339403
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
340404
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
341405
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -366,7 +430,7 @@
366430
DEVELOPMENT_TEAM = PRDQGB267K;
367431
ENABLE_PREVIEWS = YES;
368432
GENERATE_INFOPLIST_FILE = YES;
369-
INFOPLIST_KEY_CFBundleDisplayName = "OpenIAP Example";
433+
INFOPLIST_KEY_CFBundleDisplayName = OpenIAP;
370434
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
371435
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
372436
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -389,7 +453,7 @@
389453
/* End XCBuildConfiguration section */
390454

391455
/* Begin XCConfigurationList section */
392-
C0E1F5E02C8F1A9400123456 /* Build configuration list for PBXProject "OpenIapExample" */ = {
456+
C0E1F5E02C8F1A9400123456 /* Build configuration list for PBXProject "Martie" */ = {
393457
isa = XCConfigurationList;
394458
buildConfigurations = (
395459
C0E1F5F12C8F1A9500123456 /* Debug */,
1.63 MB
Loading

Example/OpenIapExample/Assets.xcassets/AppIcon.appiconset/Contents.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"images" : [
33
{
4+
"filename" : "AppIcon.png",
45
"idiom" : "universal",
56
"platform" : "ios",
67
"size" : "1024x1024"

Example/OpenIapExample/OpenIapExampleApp.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import OpenIAP
44
@main
55
@available(iOS 15.0, *)
66
struct OpenIapExampleApp: App {
7+
init() {
8+
// Enable verbose logging for the example app only
9+
OpenIapLog.setEnabled(true)
10+
}
711
var body: some Scene {
812
WindowGroup {
913
ContentView()
1014
}
1115
}
12-
}
16+
}

0 commit comments

Comments
 (0)