diff --git a/.gitignore b/.gitignore index 41d655840..578c1c52b 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,7 @@ coverage/ tmp/ temp/ .build +.swiftpm # RAG Agent generated outputs (benchmark comparisons) scripts/agent/_generated/ diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 6a85902b7..63e5afd8d 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -1,7 +1,7 @@ # OpenIAP Project Context > **Auto-generated for Claude Code** -> Last updated: 2026-06-15T14:57:16.333Z +> Last updated: 2026-06-23T15:25:38.042Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -909,7 +909,7 @@ For every new/changed handler in the generated types, verify **all five** of the | -------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **react-native-iap** | `src/types.ts` (generated) | `src/index.ts` export (Nitro or composed TS) | `ios/HybridRnIap.swift` (iOS), `android/.../HybridRnIap.kt` (Android) | Not required (flat exports) | Mock stub in all 4 `mockIap` objects in `__tests__/` (per memory) | | **expo-iap** | `src/types.ts` (generated) | `src/modules/ios.ts` / `android.ts` export, re-exported from `src/index.ts` | `ios/ExpoIapModule.swift` `AsyncFunction`, `android/.../ExpoIapModule.kt` | Not required (flat exports) | `src/modules/__tests__/*.test.ts` | -| **flutter_inapp_purchase** | `lib/types.dart` (generated) | getter on `FlutterInappPurchase` in `lib/flutter_inapp_purchase.dart` | `case "":` in `ios/Classes/FlutterInappPurchasePlugin.swift`, Android plugin `onMethodCall` | `queryHandlers` / `mutationHandlers` / `subscriptionHandlers` bundles near the bottom of `flutter_inapp_purchase.dart` | Mock + test in `test/ios_methods_test.dart` (and the `errors_unit_test.dart` error-mapping test) | +| **flutter_inapp_purchase** | `lib/types.dart` (generated) | getter on `FlutterInappPurchase` in `lib/flutter_inapp_purchase.dart` | `case "":` in `ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift` and `macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift`, Android plugin `onMethodCall` | `queryHandlers` / `mutationHandlers` / `subscriptionHandlers` bundles near the bottom of `flutter_inapp_purchase.dart` | Mock + test in `test/ios_methods_test.dart` (and the `errors_unit_test.dart` error-mapping test) | | **kmp-iap** | `library/src/commonMain/.../openiap/Types.kt` (generated interface) | exposed via `KmpInAppPurchase` / `kmpIapInstance` | `library/src/iosMain/.../InAppPurchaseIOS.kt` β€” must call `openIapModule.WithCompletion { ... }`, **never** `throw UnsupportedOperationException` | Not required (interface dispatch) | `library/src/commonTest/` if testable cross-platform | | **godot-iap** | `addons/godot-iap/types.gd` (generated) | public `snake_case` function in `addons/godot-iap/godot_iap.gd` | `ios-gdextension/Sources/GodotIap/GodotIap.swift` (iOS), `android/src/main/java/.../GodotIap.java` (Android) | Not required | Manual testing β€” no automated test suite yet | | **maui-iap** | `src/OpenIap.Maui/Types.cs` (generated) | `OpenIap.QueryResolver` / `MutationResolver` interfaces in `Types.cs`; `IOpenIap` adds the listener-stream contract; static facade is `OpenIap.Maui.OpenIapClient` (`OpenIap.Maui.Iap` remains as a legacy shim); IAPKit helpers mirror TypeScript via `OpenIapClient.KitApi(...)`, `OpenIapClient.ConnectWebhookStream(...)`, `OpenIapClient.ParseWebhookEventData(...)`, and `OpenIapClient.WebhookEventTypes` | Android: `OpenIapMauiModule.kt` in `libraries/maui-iap/android/openiap/` (JSON-shaped Java facade over `packages/google`), bound by `OpenIap.Maui.Bindings.Android.csproj`, consumed by `Platforms/Android/OpenIapAndroid.cs`. Google Billing / Play Services / Gson / AndroidX / Kotlin dependencies must stay NuGet `PackageReference`s, not fat-bundled AARs. iOS / macCatalyst: existing `OpenIapModule+ObjC.swift` bridge in `packages/apple`, bound by hand-written `OpenIap.Maui.Bindings.iOS/ApiDefinition.cs`, consumed by `Platforms/iOS/OpenIapIOS.cs` (+ subclass `OpenIapMacCatalyst`). | Not required (interface dispatch) | Example app `libraries/maui-iap/example/OpenIap.Maui.Example` builds for net9.0-android / net9.0-ios / net9.0-maccatalyst; package CI builds net9/net10 shared, Android, iOS, and macCatalyst TFMs (manual device testing for purchase flow); no xUnit tests yet | @@ -1597,6 +1597,33 @@ Godot, KMP, or MAUI versions; that manifest tracks only `spec`, `google`, and - No trailing period - Use imperative mood ("add" not "added") +## Pull Request Preview Recordings + +Every PR that introduces a new feature, visible behavior change, UI change, +documentation page, example flow, or developer workflow must include a preview +recording before it is handed off for review. + +Requirements: + +- Record the actual changed surface after the implementation is complete. Use + the Codex Chrome Extension for web/docs/dashboard previews whenever a browser + can render the change. +- Compress the final video to **under 10 MB** so GitHub accepts it reliably. + Prefer H.264 MP4 with a modest resolution / frame rate when the raw capture is + too large. +- Upload the compressed recording to the GitHub PR as a PR body attachment or a + clearly labeled attached `Preview` comment. +- Do not commit one-off PR preview recordings. Only commit preview media when + the media itself is a product documentation or example asset that should ship + with the repository. +- Link or embed the uploaded preview in the PR body or a clearly labeled + `Preview` PR comment. +- If the change has no visual or interactive surface, include a short note in + the PR explaining why a recording was not applicable and show the most useful + terminal/API proof instead. +- Do not upload secrets, private customer data, unreleased credentials, or local + browser profile details in previews. Redact or use test fixtures. + ### With Tag and Scope When a commit targets a specific package or library, include the scope: diff --git a/knowledge/internal/04-platform-packages.md b/knowledge/internal/04-platform-packages.md index afc3d6605..71cb99294 100644 --- a/knowledge/internal/04-platform-packages.md +++ b/knowledge/internal/04-platform-packages.md @@ -171,7 +171,7 @@ For every new/changed handler in the generated types, verify **all five** of the | -------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **react-native-iap** | `src/types.ts` (generated) | `src/index.ts` export (Nitro or composed TS) | `ios/HybridRnIap.swift` (iOS), `android/.../HybridRnIap.kt` (Android) | Not required (flat exports) | Mock stub in all 4 `mockIap` objects in `__tests__/` (per memory) | | **expo-iap** | `src/types.ts` (generated) | `src/modules/ios.ts` / `android.ts` export, re-exported from `src/index.ts` | `ios/ExpoIapModule.swift` `AsyncFunction`, `android/.../ExpoIapModule.kt` | Not required (flat exports) | `src/modules/__tests__/*.test.ts` | -| **flutter_inapp_purchase** | `lib/types.dart` (generated) | getter on `FlutterInappPurchase` in `lib/flutter_inapp_purchase.dart` | `case "":` in `ios/Classes/FlutterInappPurchasePlugin.swift`, Android plugin `onMethodCall` | `queryHandlers` / `mutationHandlers` / `subscriptionHandlers` bundles near the bottom of `flutter_inapp_purchase.dart` | Mock + test in `test/ios_methods_test.dart` (and the `errors_unit_test.dart` error-mapping test) | +| **flutter_inapp_purchase** | `lib/types.dart` (generated) | getter on `FlutterInappPurchase` in `lib/flutter_inapp_purchase.dart` | `case "":` in `ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift` and `macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift`, Android plugin `onMethodCall` | `queryHandlers` / `mutationHandlers` / `subscriptionHandlers` bundles near the bottom of `flutter_inapp_purchase.dart` | Mock + test in `test/ios_methods_test.dart` (and the `errors_unit_test.dart` error-mapping test) | | **kmp-iap** | `library/src/commonMain/.../openiap/Types.kt` (generated interface) | exposed via `KmpInAppPurchase` / `kmpIapInstance` | `library/src/iosMain/.../InAppPurchaseIOS.kt` β€” must call `openIapModule.WithCompletion { ... }`, **never** `throw UnsupportedOperationException` | Not required (interface dispatch) | `library/src/commonTest/` if testable cross-platform | | **godot-iap** | `addons/godot-iap/types.gd` (generated) | public `snake_case` function in `addons/godot-iap/godot_iap.gd` | `ios-gdextension/Sources/GodotIap/GodotIap.swift` (iOS), `android/src/main/java/.../GodotIap.java` (Android) | Not required | Manual testing β€” no automated test suite yet | | **maui-iap** | `src/OpenIap.Maui/Types.cs` (generated) | `OpenIap.QueryResolver` / `MutationResolver` interfaces in `Types.cs`; `IOpenIap` adds the listener-stream contract; static facade is `OpenIap.Maui.OpenIapClient` (`OpenIap.Maui.Iap` remains as a legacy shim); IAPKit helpers mirror TypeScript via `OpenIapClient.KitApi(...)`, `OpenIapClient.ConnectWebhookStream(...)`, `OpenIapClient.ParseWebhookEventData(...)`, and `OpenIapClient.WebhookEventTypes` | Android: `OpenIapMauiModule.kt` in `libraries/maui-iap/android/openiap/` (JSON-shaped Java facade over `packages/google`), bound by `OpenIap.Maui.Bindings.Android.csproj`, consumed by `Platforms/Android/OpenIapAndroid.cs`. Google Billing / Play Services / Gson / AndroidX / Kotlin dependencies must stay NuGet `PackageReference`s, not fat-bundled AARs. iOS / macCatalyst: existing `OpenIapModule+ObjC.swift` bridge in `packages/apple`, bound by hand-written `OpenIap.Maui.Bindings.iOS/ApiDefinition.cs`, consumed by `Platforms/iOS/OpenIapIOS.cs` (+ subclass `OpenIapMacCatalyst`). | Not required (interface dispatch) | Example app `libraries/maui-iap/example/OpenIap.Maui.Example` builds for net9.0-android / net9.0-ios / net9.0-maccatalyst; package CI builds net9/net10 shared, Android, iOS, and macCatalyst TFMs (manual device testing for purchase flow); no xUnit tests yet | diff --git a/libraries/flutter_inapp_purchase/.gitignore b/libraries/flutter_inapp_purchase/.gitignore index f7b63c762..8eef611b0 100644 --- a/libraries/flutter_inapp_purchase/.gitignore +++ b/libraries/flutter_inapp_purchase/.gitignore @@ -17,6 +17,7 @@ flutter_export_environment* launch.json .flutter-plugins-* +.swiftpm/ coverage # Node.js @@ -47,4 +48,4 @@ example/android/app/.cxx/ .env.* !.env.example example/env -!example/env.example \ No newline at end of file +!example/env.example diff --git a/libraries/flutter_inapp_purchase/CONTRIBUTING.md b/libraries/flutter_inapp_purchase/CONTRIBUTING.md index ba66eaaab..e67b99dbe 100644 --- a/libraries/flutter_inapp_purchase/CONTRIBUTING.md +++ b/libraries/flutter_inapp_purchase/CONTRIBUTING.md @@ -19,11 +19,16 @@ flutter pub get ### Platform-Specific Setup -#### iOS - -- This plugin uses the OpenIAP Apple native module via CocoaPods. See [openiap-versions.json](./openiap-versions.json) for the current version. -- After upgrading or cloning, run `pod install` in your iOS project (e.g., `example/ios`). -- Minimum iOS deployment target is `15.0` for StoreKit 2 support. +#### iOS and macOS + +- This plugin declares the OpenIAP Apple native module for both Swift Package Manager and CocoaPods. See [openiap-versions.json](./openiap-versions.json) for the current version. +- Flutter 3.44 and newer enables Swift Package Manager by default. When testing SwiftPM locally, run `flutter config --enable-swift-package-manager`, then build from `example`. +- CocoaPods remains supported for older Flutter projects or projects that disable SwiftPM. After upgrading or cloning, run `pod install` in each Apple example target you use: + ```bash + (cd example/ios && pod install) + (cd example/macos && pod install) + ``` +- Minimum deployment targets are iOS `15.0` and macOS `14.0`. #### Android diff --git a/libraries/flutter_inapp_purchase/Package.swift b/libraries/flutter_inapp_purchase/Package.swift deleted file mode 100644 index f87e79982..000000000 --- a/libraries/flutter_inapp_purchase/Package.swift +++ /dev/null @@ -1,30 +0,0 @@ -// swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "flutter_inapp_purchase", - platforms: [ - .iOS("12.0") - ], - products: [ - .library(name: "flutter-inapp-purchase", targets: ["flutter_inapp_purchase"]) - ], - dependencies: [], - targets: [ - .target( - name: "flutter_inapp_purchase", - dependencies: [], - path: "ios/Classes", - resources: [ - .process("../Assets") - ], - publicHeadersPath: "", - cSettings: [ - .headerSearchPath("../Flutter"), - .headerSearchPath("../../../Flutter/Export") - ] - ) - ] -) \ No newline at end of file diff --git a/libraries/flutter_inapp_purchase/README.md b/libraries/flutter_inapp_purchase/README.md index 1ad1ed646..24ea68893 100644 --- a/libraries/flutter_inapp_purchase/README.md +++ b/libraries/flutter_inapp_purchase/README.md @@ -24,6 +24,25 @@ flutter pub add flutter_inapp_purchase For manual `pubspec.yaml` edits, copy the current dependency from the [flutter_inapp_purchase pub.dev package page](https://pub.dev/packages/flutter_inapp_purchase). +### iOS/macOS Native Dependency Resolution + +No manual `Package.swift` or `Podfile` entry is required. On Flutter 3.44 and +newer, Swift Package Manager is enabled by default and the Flutter CLI resolves +the native OpenIAP dependency automatically when you run or build the app. + +Projects that disable Swift Package Manager, or projects using an older Flutter +toolchain, continue to use CocoaPods. Run `pod install` after `flutter pub get` +for each Apple target you use: + +```bash +(cd ios && pod install) + +# If your app also has a macOS target: +(cd macos && pod install) +``` + +Apple platform targets require iOS 15.0+ or macOS 14.0+. + ## πŸ”§ Quick Start ### Basic Usage @@ -61,6 +80,7 @@ flutter_inapp_purchase provides AI-friendly documentation for Cursor, GitHub Cop **[AI Assistants Guide](https://openiap.dev/docs/guides/ai-assistants)** Quick links: + - [llms.txt](https://openiap.dev/llms.txt) - Quick reference - [llms-full.txt](https://openiap.dev/llms-full.txt) - Full API reference diff --git a/libraries/flutter_inapp_purchase/example/.gitignore b/libraries/flutter_inapp_purchase/example/.gitignore index dee655cc4..3dc945995 100644 --- a/libraries/flutter_inapp_purchase/example/.gitignore +++ b/libraries/flutter_inapp_purchase/example/.gitignore @@ -7,3 +7,6 @@ build/ .flutter-plugins + +# Local env file with API keys - copy from env.example and fill in values +env diff --git a/libraries/flutter_inapp_purchase/example/ios/.gitignore b/libraries/flutter_inapp_purchase/example/ios/.gitignore index 79cc4da80..8b4f7ad29 100644 --- a/libraries/flutter_inapp_purchase/example/ios/.gitignore +++ b/libraries/flutter_inapp_purchase/example/ios/.gitignore @@ -38,8 +38,10 @@ Icon? /Flutter/flutter_assets/ /Flutter/App.framework /Flutter/Flutter.framework +/Flutter/ephemeral/Packages/ /Flutter/Generated.xcconfig /ServiceDefinitions.json Pods/ .symlinks/ +**/xcshareddata/swiftpm/ diff --git a/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/project.pbxproj b/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/project.pbxproj index 21541b124..55a7267ec 100644 --- a/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/project.pbxproj +++ b/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 2D5378271FAD1A9400D5DBA9 /* StoreKit.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAD1A9400D5DBA9 /* StoreKit.storekit */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3CCED4E62E30A89200CE4F79 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CCED4E52E30A89200CE4F79 /* StoreKit.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -42,6 +43,9 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3CCED4E52E30A89200CE4F79 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 49A6FFBAD3488E7D435E2D63 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = Flutter/ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; + 78DABEA22ED26510000E7860 /* flutter_inapp_purchase */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = flutter_inapp_purchase; path = ../../ios/flutter_inapp_purchase; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -62,6 +66,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 3CCED4E62E30A89200CE4F79 /* StoreKit.framework in Frameworks */, 079B74431F6B5F836BD33DBC /* Pods_Runner.framework in Frameworks */, ); @@ -82,6 +87,9 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78DABEA22ED26510000E7860 /* flutter_inapp_purchase */, + 784666492D4C4C64000A1A5F /* FlutterFramework */, + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -165,6 +173,9 @@ dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -198,6 +209,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -248,12 +262,10 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/flutter_inapp_purchase/flutter_inapp_purchase.framework", "${BUILT_PRODUCTS_DIR}/openiap/OpenIAP.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inapp_purchase.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenIAP.framework", ); runOnlyForDeploymentPostprocessing = 0; @@ -511,6 +523,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1a629b631..3f2248af8 100644 --- a/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/libraries/flutter_inapp_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/libraries/flutter_inapp_purchase/example/ios/Runner/AppDelegate.m b/libraries/flutter_inapp_purchase/example/ios/Runner/AppDelegate.m index 59a72e90b..64973b989 100644 --- a/libraries/flutter_inapp_purchase/example/ios/Runner/AppDelegate.m +++ b/libraries/flutter_inapp_purchase/example/ios/Runner/AppDelegate.m @@ -5,9 +5,13 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; + return [super application:application + didFinishLaunchingWithOptions:launchOptions]; +} +- (void)didInitializeImplicitFlutterEngine: + (NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; } - @end diff --git a/libraries/flutter_inapp_purchase/example/ios/Runner/Info.plist b/libraries/flutter_inapp_purchase/example/ios/Runner/Info.plist index 4d62b3f9d..40fb0311e 100644 --- a/libraries/flutter_inapp_purchase/example/ios/Runner/Info.plist +++ b/libraries/flutter_inapp_purchase/example/ios/Runner/Info.plist @@ -52,5 +52,26 @@ cstr6suwn9.skadnetwork + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/libraries/flutter_inapp_purchase/example/macos/.gitignore b/libraries/flutter_inapp_purchase/example/macos/.gitignore index 746adbb6b..36bc3c5b0 100644 --- a/libraries/flutter_inapp_purchase/example/macos/.gitignore +++ b/libraries/flutter_inapp_purchase/example/macos/.gitignore @@ -5,3 +5,4 @@ # Xcode-related **/dgph **/xcuserdata/ +**/xcshareddata/swiftpm/ diff --git a/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/project.pbxproj b/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/project.pbxproj index 6e9e94798..5e1ce7fa4 100644 --- a/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/project.pbxproj +++ b/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; D0626713F80DD2BD49E91E5F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5637C7071BCB7780E65D7EB /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -78,6 +79,9 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; + 78DABEA22ED26510000E7860 /* flutter_inapp_purchase */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = flutter_inapp_purchase; path = ../../../macos/flutter_inapp_purchase; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 8464B893525EE848211C4C1B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; @@ -103,6 +107,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 157D99104874B164A5EC7598 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -178,6 +183,9 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78DABEA22ED26510000E7860 /* flutter_inapp_purchase */, + 784666492D4C4C64000A1A5F /* FlutterFramework */, + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -240,7 +248,6 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 45BEBE97C1FC6AE6D9522D26 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -248,6 +255,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* flutter_inapp_purchase_example.app */; productType = "com.apple.product-type.application"; @@ -292,6 +302,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -384,23 +397,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 45BEBE97C1FC6AE6D9522D26 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 49A51C8CD36F5E5A826ADFB4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -797,6 +793,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4dfe7012e..0b7220cea 100644 --- a/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/libraries/flutter_inapp_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + '../LICENSE' } s.author = { 'Hyo Dev' => 'hyo@hyo.dev' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*.swift' + s.source_files = 'flutter_inapp_purchase/Sources/flutter_inapp_purchase/**/*.swift' s.dependency 'Flutter' # Use OpenIAP Apple native module (via CocoaPods) s.dependency 'openiap', openiap_versions['apple'] diff --git a/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Package.swift b/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Package.swift new file mode 100644 index 000000000..f54e792c9 --- /dev/null +++ b/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "flutter_inapp_purchase", + platforms: [ + .iOS("15.0"), + ], + products: [ + .library(name: "flutter-inapp-purchase", targets: ["flutter_inapp_purchase"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), + .package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1"), + ], + targets: [ + .target( + name: "flutter_inapp_purchase", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), + .product(name: "OpenIAP", package: "OpenIAP"), + ], + path: "Sources/flutter_inapp_purchase" + ) + ] +) diff --git a/libraries/flutter_inapp_purchase/ios/Classes/FlutterIapHelper.swift b/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift similarity index 100% rename from libraries/flutter_inapp_purchase/ios/Classes/FlutterIapHelper.swift rename to libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift diff --git a/libraries/flutter_inapp_purchase/ios/Classes/FlutterIapLog.swift b/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapLog.swift similarity index 100% rename from libraries/flutter_inapp_purchase/ios/Classes/FlutterIapLog.swift rename to libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapLog.swift diff --git a/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift b/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift similarity index 100% rename from libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift rename to libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift diff --git a/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase.podspec b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase.podspec index 9c8ab76e9..5523feccf 100644 --- a/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase.podspec +++ b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase.podspec @@ -22,7 +22,7 @@ In App Purchase plugin for flutter. This project has been forked by react-native s.license = { :file => '../LICENSE' } s.author = { 'Hyo Dev' => 'hyo@hyo.dev' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*.swift' + s.source_files = 'flutter_inapp_purchase/Sources/flutter_inapp_purchase/**/*.swift' s.dependency 'FlutterMacOS' # Use OpenIAP Apple native module (via CocoaPods) s.dependency 'openiap', openiap_versions['apple'] diff --git a/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Package.swift b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Package.swift new file mode 100644 index 000000000..9a198039f --- /dev/null +++ b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "flutter_inapp_purchase", + platforms: [ + .macOS("14.0"), + ], + products: [ + .library(name: "flutter-inapp-purchase", targets: ["flutter_inapp_purchase"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), + .package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1"), + ], + targets: [ + .target( + name: "flutter_inapp_purchase", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), + .product(name: "OpenIAP", package: "OpenIAP"), + ], + path: "Sources/flutter_inapp_purchase" + ) + ] +) diff --git a/libraries/flutter_inapp_purchase/macos/Classes/FlutterIapHelper.swift b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift similarity index 100% rename from libraries/flutter_inapp_purchase/macos/Classes/FlutterIapHelper.swift rename to libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift diff --git a/libraries/flutter_inapp_purchase/macos/Classes/FlutterIapLog.swift b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapLog.swift similarity index 100% rename from libraries/flutter_inapp_purchase/macos/Classes/FlutterIapLog.swift rename to libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapLog.swift diff --git a/libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift similarity index 100% rename from libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift rename to libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift diff --git a/llms-full.txt b/llms-full.txt deleted file mode 100644 index 86d8fb5c1..000000000 --- a/llms-full.txt +++ /dev/null @@ -1,2214 +0,0 @@ -# OpenIAP Complete Reference - -> OpenIAP: Unified in-app purchase specification for iOS & Android -> Documentation: https://openiap.dev -> Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-06-15T14:57:16.350Z - -## Table of Contents -1. Installation -2. Core APIs (Connection, Products, Purchase, Subscription) -3. Platform-Specific APIs (iOS, Android) -4. Types Reference -5. Error Codes & Handling -6. Implementation Patterns - ---- - -## 1. Installation - -### React Native / Expo -```bash -# expo-iap (Expo projects - recommended) -npx expo install expo-iap - -# react-native-iap (React Native CLI) -npm install react-native-iap -cd ios && pod install -``` - -### Swift (iOS/macOS) -```swift -// Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") - -// CocoaPods -pod 'openiap', '~> 2.2.1' -``` - -### Kotlin (Android) -```kotlin -// Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.2.1") - -// For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.1") -``` - -### Flutter -```bash -flutter pub add flutter_inapp_purchase -``` - -### Godot -Download `godot-iap-2.3.1.zip` from GitHub Releases, extract it to -`addons/godot-iap/`, then enable the plugin in Project Settings. - -### Kotlin Multiplatform -```kotlin -dependencies { - implementation("io.github.hyochan:kmp-iap:2.3.1") -} -``` - -Use the latest version from Maven Central: -https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap - -### .NET MAUI -```bash -dotnet add package OpenIap.Maui -``` - -Current NuGet package version: 1.1.1 - -Requires .NET 9 or .NET 10, the MAUI workload, iOS 15.0+, and Android API 24+. - ---- - -## Framework SDK Implementations - -### react-native-iap -- Package: `react-native-iap` on npm. -- Implementation: Nitro Modules wrapper over `packages/apple` and - `packages/google`. -- Public surface: generated OpenIAP types plus `useIAP`, listener helpers, - and platform-suffixed iOS/Android APIs. -- Example app: `libraries/react-native-iap/example`. - -### expo-iap -- Package: `expo-iap` on npm. -- Implementation: Expo Modules wrapper over the same native OpenIAP packages. -- Public surface: same hook, listener, query, mutation, and platform API - shape as `react-native-iap`, adapted for Expo managed/bare workflows. -- Example app: `libraries/expo-iap/example`. - -### flutter_inapp_purchase -- Package: `flutter_inapp_purchase` on pub.dev. -- Implementation: Dart API plus generated `types.dart`, bridged to native - iOS and Android method channels. -- Public surface: singleton `FlutterInappPurchase.instance`, typed - `fetchProducts`, purchase streams, and resolver-style methods. - -### godot-iap -- Package: `godot-iap` for Godot 4.x. -- Implementation: GDScript API with generated `types.gd`, plus native iOS - GDExtension and Android AAR plugin. -- Public surface: snake_case functions and Godot signals matching OpenIAP. - -### kmp-iap -- Package: `io.github.hyochan:kmp-iap`. -- Implementation: Kotlin Multiplatform common API with Flow-based events, - Android implementation, and iOS cinterop through the OpenIAP ObjC facade. -- Public surface: `KmpIAP` / shared instance resolver methods and flows. - -### maui-iap -- Package: `OpenIap.Maui` on NuGet. -- Distribution: single public NuGet package. The Android/iOS binding projects - are private implementation details and are flattened into `OpenIap.Maui` - instead of being published as separate package dependencies. -- Implementation: .NET MAUI projection with generated `Types.cs`, a static - `OpenIapClient.Instance` facade, legacy `Iap` shim, `IOpenIap` - observables, and per-platform resolvers. -- iOS/macCatalyst bridge: .NET-for-iOS binding over - `OpenIAP.xcframework` and `OpenIapModule+ObjC.swift`; NuGet consumers get - the official `OpenIap.Maui.Bindings.iOS.resources.zip` sidecar so no - app-level `NativeReference` is required. -- Android bridge: Xamarin.Android binding over the MAUI-owned - `openiap-release.aar`, which wraps the unbound - `openiap-play-release.aar` runtime dependency. Google Billing, Play - Services, Gson, AndroidX, and Kotlin Android libraries stay as NuGet - `PackageReference` dependencies so consuming apps can deduplicate them. -- Public surface: `QueryResolver`, `MutationResolver`, and `IOpenIap` - implemented by `OpenIapIOS`, `OpenIapAndroid`, and `OpenIapMacCatalyst`; - IAPKit helpers mirror the TypeScript SDKs via - `OpenIapClient.KitApi(...)`, `OpenIapClient.ConnectWebhookStream(...)`, - `OpenIapClient.ParseWebhookEventData(...)`, and - `OpenIapClient.WebhookEventTypes`. -- Example app: `libraries/maui-iap/example/OpenIap.Maui.Example`, mirroring - the `expo-iap` example flows. - ---- - -## Minimal Usage by Framework - -### React Native / Expo -```typescript -import { useIAP } from 'expo-iap'; // or 'react-native-iap' - -const { connected, fetchProducts, requestPurchase, finishTransaction } = useIAP({ - onPurchaseSuccess: async (purchase) => { - await finishTransaction({ purchase, isConsumable: true }); - }, -}); - -await fetchProducts({ skus: ['premium'], type: 'in-app' }); -await requestPurchase({ - request: { ios: { sku: 'premium' }, android: { skus: ['premium'] } }, - type: 'in-app', -}); -``` - -### Flutter -```dart -final iap = FlutterInappPurchase.instance; -await iap.initConnection(); -final products = await iap.fetchProducts( - skus: ['premium'], - type: ProductQueryType.inApp, -); -iap.purchaseUpdated.listen((purchase) async { - await iap.finishTransaction(purchase, isConsumable: true); -}); -``` - -### Godot -```gdscript -GodotIapPlugin.purchase_updated.connect(_on_purchase_updated) -GodotIapPlugin.init_connection() -GodotIapPlugin.fetch_products(request) -GodotIapPlugin.request_purchase(props) -``` - -### Kotlin Multiplatform -```kotlin -val iap = KmpIAP() -iap.initConnection() -val products = iap.fetchProducts(skus = listOf("premium")) -iap.purchaseUpdatedListener.collect { purchase -> - iap.finishTransaction(purchase = purchase, isConsumable = true) -} -``` - -### .NET MAUI -```csharp -using OpenIap; -using OpenIap.Maui; - -var iap = OpenIapClient.Instance; -await ((MutationResolver)iap).InitConnectionAsync(); - -await ((QueryResolver)iap).FetchProductsAsync(new ProductRequest -{ - Skus = ["premium"], - Type = ProductQueryType.InApp, -}); - -((IOpenIap)iap).PurchaseUpdated.Subscribe(async purchase => -{ - await ((MutationResolver)iap).FinishTransactionAsync( - new PurchaseInput(purchase), - isConsumable: true - ); -}); -``` - ---- - -# expo-iap API Reference - -> Reference documentation for expo-iap (Expo In-App Purchase module) -> Adapt all patterns to match OpenIAP internal conventions. - -## Overview - -expo-iap is the Expo-compatible version of react-native-iap, providing in-app purchase functionality for both iOS and Android in Expo projects. - -## Installation - -```bash -npx expo install expo-iap -``` - -## Connection Management - -### initConnection - -Initialize connection to the app store. - -```typescript -import { initConnection } from 'expo-iap'; - -await initConnection(); -``` - -### endConnection - -Close connection to the app store. - -```typescript -import { endConnection } from 'expo-iap'; - -await endConnection(); -``` - -## Product Operations - -### fetchProducts - -Fetch product information from the store. - -```typescript -import { fetchProducts } from 'expo-iap'; - -const products = await fetchProducts(['com.app.product1', 'com.app.sub_monthly']); -``` - -**Returns:** `Promise` - -### Product Type - -```typescript -interface Product { - productId: string; - title: string; - description: string; - price: string; - currency: string; - localizedPrice: string; - type: ProductType; // 'iap' | 'sub' - - // iOS only - subscriptionPeriodNumberIOS?: string; - subscriptionPeriodUnitIOS?: string; - introductoryPrice?: string; - introductoryPricePaymentModeIOS?: string; - introductoryPriceNumberOfPeriodsIOS?: string; - introductoryPriceSubscriptionPeriodIOS?: string; - - // Android only - subscriptionOfferDetailsAndroid?: SubscriptionOffer[]; - oneTimePurchaseOfferDetailsAndroid?: OneTimePurchaseOffer; -} -``` - -## Purchase Operations - -### requestPurchase - -Initiate a purchase. - -```typescript -import { requestPurchase } from 'expo-iap'; - -// For consumables/non-consumables -await requestPurchase({ sku: 'com.app.product1' }); - -// For subscriptions (Android) -await requestPurchase({ - sku: 'com.app.sub_monthly', - subscriptionOffers: [{ sku: 'com.app.sub_monthly', offerToken: 'token' }] -}); -``` - -### finishTransaction - -Complete a transaction after processing. - -```typescript -import { finishTransaction } from 'expo-iap'; - -await finishTransaction({ purchase, isConsumable: true }); -``` - -### getAvailablePurchases - -Get user's existing purchases (restore purchases). - -```typescript -import { getAvailablePurchases } from 'expo-iap'; - -const purchases = await getAvailablePurchases(); -``` - -## Purchase Type - -```typescript -interface Purchase { - productId: string; - transactionId?: string; - transactionDate: number; - transactionReceipt: string; - purchaseToken?: string; // Android - - // iOS only - originalTransactionDateIOS?: number; - originalTransactionIdentifierIOS?: string; - - // Android only - purchaseStateAndroid?: number; - isAcknowledgedAndroid?: boolean; - packageNameAndroid?: string; - obfuscatedAccountIdAndroid?: string; - obfuscatedProfileIdAndroid?: string; -} -``` - -## iOS-Specific Functions - -### clearTransactionIOS - -Clear finished transactions from the queue. - -```typescript -import { clearTransactionIOS } from 'expo-iap'; - -await clearTransactionIOS(); -``` - -### getReceiptDataIOS - -Get the receipt data for validation. - -```typescript -import { getReceiptDataIOS } from 'expo-iap'; - -const receipt = await getReceiptDataIOS(); -``` - -### syncIOS - -Sync transactions with the App Store. - -```typescript -import { syncIOS } from 'expo-iap'; - -await syncIOS(); -``` - -### presentCodeRedemptionSheetIOS - -Show the offer code redemption sheet. - -```typescript -import { presentCodeRedemptionSheetIOS } from 'expo-iap'; - -await presentCodeRedemptionSheetIOS(); -``` - -### showManageSubscriptionsIOS - -Open subscription management in App Store. - -```typescript -import { showManageSubscriptionsIOS } from 'expo-iap'; - -await showManageSubscriptionsIOS(); -``` - -### isEligibleForIntroOfferIOS - -Check intro offer eligibility. - -```typescript -import { isEligibleForIntroOfferIOS } from 'expo-iap'; - -const eligible = await isEligibleForIntroOfferIOS('com.app.sub_monthly'); -``` - -### beginRefundRequestIOS - -Start a refund request. - -```typescript -import { beginRefundRequestIOS } from 'expo-iap'; - -const result = await beginRefundRequestIOS('transaction_id'); -``` - -## Android-Specific Functions - -### acknowledgePurchaseAndroid - -Acknowledge a purchase (required within 3 days). - -```typescript -import { acknowledgePurchaseAndroid } from 'expo-iap'; - -await acknowledgePurchaseAndroid({ token: purchase.purchaseToken }); -``` - -### consumePurchaseAndroid - -Consume a consumable purchase. - -```typescript -import { consumePurchaseAndroid } from 'expo-iap'; - -await consumePurchaseAndroid({ token: purchase.purchaseToken }); -``` - -### getPackageNameAndroid - -Get the app's package name. - -```typescript -import { getPackageNameAndroid } from 'expo-iap'; - -const packageName = await getPackageNameAndroid(); -``` - -## Cross-Platform Functions - -### getActiveSubscriptions - -Get active subscriptions. - -```typescript -import { getActiveSubscriptions } from 'expo-iap'; - -const subscriptions = await getActiveSubscriptions(['com.app.sub_monthly']); -``` - -### hasActiveSubscriptions - -Check if user has active subscriptions. - -```typescript -import { hasActiveSubscriptions } from 'expo-iap'; - -const hasActive = await hasActiveSubscriptions(['com.app.sub_monthly']); -``` - -### deepLinkToSubscriptions - -Open subscription management on both platforms. - -```typescript -import { deepLinkToSubscriptions } from 'expo-iap'; - -await deepLinkToSubscriptions({ sku: 'com.app.sub_monthly' }); -``` - -### getStorefront - -Get storefront information. - -```typescript -import { getStorefront } from 'expo-iap'; - -const storefront = await getStorefront(); -// { countryCode: 'US', ... } -``` - -## Event Listeners - -### purchaseUpdatedListener - -Listen for purchase updates. - -```typescript -import { purchaseUpdatedListener } from 'expo-iap'; - -const subscription = purchaseUpdatedListener((purchase) => { - console.log('Purchase updated:', purchase); - // Process and finish transaction -}); - -// Cleanup -subscription.remove(); -``` - -### purchaseErrorListener - -Listen for purchase errors. - -```typescript -import { purchaseErrorListener } from 'expo-iap'; - -const subscription = purchaseErrorListener((error) => { - console.error('Purchase error:', error); -}); - -// Cleanup -subscription.remove(); -``` - -## Error Codes - -| Code | Description | -|------|-------------| -| `E_UNKNOWN` | Unknown error | -| `E_USER_CANCELLED` | User cancelled | -| `E_ITEM_UNAVAILABLE` | Item not available | -| `E_NETWORK_ERROR` | Network error | -| `E_SERVICE_ERROR` | Store service error | -| `E_ALREADY_OWNED` | Item already owned | -| `E_NOT_PREPARED` | Not initialized | -| `E_NOT_ENDED` | Connection not ended | -| `E_DEVELOPER_ERROR` | Developer error | - -## Usage Pattern - -```typescript -import { - initConnection, - endConnection, - fetchProducts, - requestPurchase, - finishTransaction, - purchaseUpdatedListener, - purchaseErrorListener, -} from 'expo-iap'; - -// Setup -await initConnection(); - -const purchaseListener = purchaseUpdatedListener(async (purchase) => { - // Verify purchase server-side - // Then finish transaction - await finishTransaction({ purchase, isConsumable: false }); -}); - -const errorListener = purchaseErrorListener((error) => { - console.error(error); -}); - -// Fetch products -const products = await fetchProducts(['com.app.premium']); - -// Make purchase -await requestPurchase({ sku: 'com.app.premium' }); - -// Cleanup -purchaseListener.remove(); -errorListener.remove(); -await endConnection(); -``` - - ---- - -# Google Play Billing Library API Reference - -> Reference documentation for Google Play Billing Library 8.x -> Adapt all patterns to match OpenIAP internal conventions. - -## Overview - -Google Play Billing Library enables in-app purchases and subscriptions on Android devices. - -## Version History - -| Version | Release Date | Key Features | -|---------|--------------|--------------| -| 8.0 | 2025-06-30 | Auto-reconnect, product-level status codes, one-time products with multiple offers, sub-response codes | -| 8.1 | 2025-11-06 | Suspended subscriptions (`isSuspended`), `includeSuspended` parameter, pre-order details, product-level subscription replacement, `KEEP_EXISTING` mode | -| 8.2 | 2025-12-09 | Billing Programs API (external content links, external offers), deprecates old External Offers API | -| 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` | -| 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options | - -**Current Version**: 8.3.0 (as of April 2026) - -## Core Classes - -### BillingClient - -The main interface for communicating with Google Play Billing. - -```kotlin -val billingClient = BillingClient.newBuilder(context) - .setListener(purchasesUpdatedListener) - .enablePendingPurchases() - // New in 8.0: Auto-reconnect on service disconnect - .enableAutoServiceReconnection() - .build() -``` - -### Auto Service Reconnection (8.0+) - -```kotlin -// Enables automatic reconnection when service disconnects -BillingClient.newBuilder(context) - .enableAutoServiceReconnection() - .build() -``` - -When enabled, the library automatically re-establishes the connection if an API call is made while disconnected. This reduces `SERVICE_DISCONNECTED` errors. - -> **OpenIAP Note**: Auto-reconnection is **always enabled** internally since OpenIAP uses Billing Library 8.3.0+. No configuration needed. - -### Connection Management - -```kotlin -billingClient.startConnection(object : BillingClientStateListener { - override fun onBillingSetupFinished(billingResult: BillingResult) { - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { - // Ready to query purchases - } - } - - override fun onBillingServiceDisconnected() { - // Reconnect on next request - } -}) -``` - -## Product Details - -### QueryProductDetailsParams - -```kotlin -val productList = listOf( - QueryProductDetailsParams.Product.newBuilder() - .setProductId("product_id") - .setProductType(BillingClient.ProductType.SUBS) // or INAPP - .build() -) - -val params = QueryProductDetailsParams.newBuilder() - .setProductList(productList) - .build() - -billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList -> - // Handle product details -} -``` - -### ProductDetails Properties - -| Property | Type | Description | -|----------|------|-------------| -| `productId` | String | Unique product identifier | -| `productType` | String | "subs" or "inapp" | -| `title` | String | Localized product title | -| `name` | String | Product name | -| `description` | String | Localized description | -| `oneTimePurchaseOfferDetails` | Object | For INAPP products | -| `subscriptionOfferDetails` | List | For subscription products | - -### Subscription Offer Details - -```kotlin -data class SubscriptionOfferDetails( - val basePlanId: String, - val offerId: String?, - val offerToken: String, - val pricingPhases: PricingPhases, - val offerTags: List -) -``` - -### Pricing Phases - -```kotlin -data class PricingPhase( - val formattedPrice: String, - val priceAmountMicros: Long, - val priceCurrencyCode: String, - val billingPeriod: String, // ISO 8601 (P1W, P1M, P1Y) - val billingCycleCount: Int, - val recurrenceMode: Int // FINITE or INFINITE -) -``` - -## Purchase Flow - -### Launch Purchase Flow - -```kotlin -val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder() - .setProductDetails(productDetails) - .setOfferToken(offerToken) // For subscriptions - .build() - -val billingFlowParams = BillingFlowParams.newBuilder() - .setProductDetailsParamsList(listOf(productDetailsParams)) - .build() - -val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams) -``` - -### PurchasesUpdatedListener - -```kotlin -val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> - when (billingResult.responseCode) { - BillingClient.BillingResponseCode.OK -> { - purchases?.forEach { purchase -> - handlePurchase(purchase) - } - } - BillingClient.BillingResponseCode.USER_CANCELED -> { - // User cancelled - } - else -> { - // Handle error - } - } -} -``` - -## Purchase Verification & Acknowledgement - -### Verify Purchase - -```kotlin -val purchase: Purchase - -// Check purchase state -if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { - // Verify signature server-side - // Then acknowledge or consume -} -``` - -### Acknowledge Purchase (Subscriptions/Non-consumables) - -```kotlin -if (!purchase.isAcknowledged) { - val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() - .setPurchaseToken(purchase.purchaseToken) - .build() - - billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult -> - // Handle result - } -} -``` - -### Consume Purchase (Consumables) - -```kotlin -val consumeParams = ConsumeParams.newBuilder() - .setPurchaseToken(purchase.purchaseToken) - .build() - -billingClient.consumeAsync(consumeParams) { billingResult, purchaseToken -> - // Handle result -} -``` - -## Query Existing Purchases - -```kotlin -// Query subscriptions -billingClient.queryPurchasesAsync( - QueryPurchasesParams.newBuilder() - .setProductType(BillingClient.ProductType.SUBS) - .build() -) { billingResult, purchasesList -> - // Handle existing subscriptions -} - -// Query in-app products -billingClient.queryPurchasesAsync( - QueryPurchasesParams.newBuilder() - .setProductType(BillingClient.ProductType.INAPP) - .build() -) { billingResult, purchasesList -> - // Handle existing purchases -} -``` - -## Purchase Properties - -| Property | Type | Description | -|----------|------|-------------| -| `orderId` | String | Unique order identifier | -| `purchaseToken` | String | Token for verification | -| `purchaseState` | Int | PENDING, PURCHASED, UNSPECIFIED | -| `purchaseTime` | Long | Timestamp in milliseconds | -| `products` | List | Product IDs in purchase | -| `isAcknowledged` | Boolean | Whether acknowledged | -| `isAutoRenewing` | Boolean | Auto-renewal status | -| `quantity` | Int | Quantity purchased | - -## Response Codes - -| Code | Constant | Description | -|------|----------|-------------| -| 0 | OK | Success | -| 1 | USER_CANCELED | User cancelled | -| 2 | SERVICE_UNAVAILABLE | Network error | -| 3 | BILLING_UNAVAILABLE | Billing not available | -| 4 | ITEM_UNAVAILABLE | Item not available | -| 5 | DEVELOPER_ERROR | Invalid arguments | -| 6 | ERROR | Fatal error | -| 7 | ITEM_ALREADY_OWNED | Already owned | -| 8 | ITEM_NOT_OWNED | Not owned | - -## Feature Support - -```kotlin -// Check if feature is supported -val result = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS) -if (result.responseCode == BillingClient.BillingResponseCode.OK) { - // Subscriptions are supported -} -``` - -### Feature Types - -- `SUBSCRIPTIONS` - Subscription support -- `SUBSCRIPTIONS_UPDATE` - Subscription upgrades/downgrades -- `PRICE_CHANGE_CONFIRMATION` - Price change confirmation -- `PRODUCT_DETAILS` - Product details API - -## Product-Level Status Codes (8.0+) - -In Billing Library 8.0+, `queryProductDetailsAsync()` returns products that couldn't be fetched with a status code explaining why. - -```kotlin -billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList -> - productDetailsList.forEach { productDetails -> - when (productDetails.productStatus) { - ProductDetails.ProductStatus.OK -> { - // Product fetched successfully - } - ProductDetails.ProductStatus.NOT_FOUND -> { - // SKU doesn't exist in Play Console - } - ProductDetails.ProductStatus.NO_OFFERS_AVAILABLE -> { - // User not eligible for any offers - } - } - } -} -``` - -| Status | Description | -|--------|-------------| -| `OK` | Product fetched successfully | -| `NOT_FOUND` | SKU doesn't exist in Play Console | -| `NO_OFFERS_AVAILABLE` | User not eligible for any offers | - -## Suspended Subscriptions (8.1+) - -```kotlin -val purchase: Purchase - -// Check if subscription is suspended due to billing issue -if (purchase.isSuspended) { - // User's payment method failed - // Do NOT grant entitlements - // Direct user to subscription center to fix payment -} -``` - -### Query Suspended Subscriptions (8.1+) - -```kotlin -// Include suspended subscriptions in query results -val params = QueryPurchasesParams.newBuilder() - .setProductType(BillingClient.ProductType.SUBS) - .setIncludeSuspended(true) // New in 8.1 - .build() - -billingClient.queryPurchasesAsync(params) { billingResult, purchases -> - purchases.forEach { purchase -> - if (purchase.isSuspended) { - // Handle suspended subscription - } - } -} -``` - -> **OpenIAP Note**: Use `includeSuspendedAndroid: true` in `PurchaseOptions` when calling `getAvailablePurchases()`. The `isSuspendedAndroid` field on purchases indicates suspension status. - -## Sub-Response Codes (8.0+) - -`BillingResult` includes a sub-response code for more granular error information: - -```kotlin -val result = billingClient.launchBillingFlow(activity, params) -when (result.subResponseCode) { - BillingResult.SUB_RESPONSE_CODE_INSUFFICIENT_FUNDS -> { - // User's payment method has insufficient funds - } - BillingResult.SUB_RESPONSE_CODE_USER_INELIGIBLE -> { - // User doesn't meet offer eligibility requirements - } -} -``` - -| Sub-Response Code | Description | -|-------------------|-------------| -| `PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS` | User's payment method has insufficient funds | -| `USER_INELIGIBLE` | User doesn't meet subscription offer eligibility | -| `NO_APPLICABLE_SUB_RESPONSE_CODE` | No specific sub-code applies | - -## Subscription Product Replacement (8.1+) - -Product-level replacement parameters for subscription upgrades/downgrades: - -```kotlin -val replacementParams = SubscriptionProductReplacementParams.newBuilder() - .setOldProductId("old_subscription_id") - .setReplacementMode(ReplacementMode.WITH_TIME_PRORATION) - .build() - -val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder() - .setProductDetails(newProductDetails) - .setOfferToken(offerToken) - .setSubscriptionProductReplacementParams(replacementParams) // New in 8.1 - .build() -``` - -### Replacement Modes - -| Mode | Description | -|------|-------------| -| `WITH_TIME_PRORATION` | Immediate, expiration time prorated | -| `CHARGE_PRORATED_PRICE` | Immediate, same billing cycle | -| `CHARGE_FULL_PRICE` | Immediate, full price charged | -| `WITHOUT_PRORATION` | Takes effect on old plan expiration | -| `DEFERRED` | Deferred, no charge | -| `KEEP_EXISTING` | Keep existing payment schedule (8.1+) | - -## External Payments Program (8.3+) - -Billing Library 8.3 (December 2025) added support for the External Payments program (Japan-only, as of launch). Developers enrolled in the program can offer alternative payment methods alongside Google Play billing. - -### Enable Developer Billing Option - -```kotlin -// During BillingClient setup -val billingClient = BillingClient.newBuilder(context) - .setListener(purchasesUpdatedListener) - .enablePendingPurchases() - .enableAutoServiceReconnection() - .enableDeveloperBillingOption( - DeveloperBillingOptionParams.newBuilder() - .setDeveloperProvidedBillingListener(developerBillingListener) - .build() - ) - .build() -``` - -### DeveloperProvidedBillingListener - -```kotlin -val developerBillingListener = DeveloperProvidedBillingListener { - userInitiatedBillingDetails -> - // User chose the developer-provided billing flow. - // Launch your external payment UI here. -} -``` - -### Launch Purchase with External Payments Option - -```kotlin -val params = BillingFlowParams.newBuilder() - .setProductDetailsParamsList(listOf(productDetailsParams)) - .setBillingOption(BillingOption.EXTERNAL_PAYMENTS) // 8.3+ - .build() - -billingClient.launchBillingFlow(activity, params) -``` - -### Key Types (8.3+) - -| Type | Purpose | -|------|---------| -| `DeveloperBillingOptionParams` | Configures developer-billing support on `BillingClient` | -| `DeveloperProvidedBillingListener` | Callback when user picks developer-provided billing | -| `DeveloperProvidedBillingDetails` | Billing details to report back for reconciliation | -| `BillingOption.EXTERNAL_PAYMENTS` | Purchase-flow flag requesting external payments | - -> **OpenIAP Note**: Exposed through the Android-specific `AlternativeBilling*` surface in OpenIAP. Enrolment with Google Play's External Payments program is required; availability is currently restricted to Japan. The Horizon flavor does NOT implement this. - -## Best Practices - -1. **Always acknowledge purchases** within 3 days or they will be refunded -2. **Verify purchases server-side** using Google Play Developer API -3. **Handle pending purchases** for payment methods that require additional steps -4. **Auto-reconnect is enabled by default** in OpenIAP (8.0+) -5. **Check product status codes** (8.0+) to understand why products weren't fetched -6. **Check isSuspended** (8.1+) before granting entitlements -7. **Cache product details** to avoid repeated queries - - ---- - -# Meta Horizon IAP API Reference - -> External reference for Meta Horizon Store in-app purchase APIs. -> Source: [Meta Horizon Documentation](https://developers.meta.com/horizon/documentation/) - -## Overview - -Meta Horizon provides IAP functionality for Quest VR applications. There are two main integration paths: - -1. **Platform SDK IAP** - Native Horizon IAP APIs -2. **Billing Compatibility SDK** - Google Play Billing Library-compatible wrapper - -## Version Compatibility Matrix - -| Library | Version | Compatible With | -|---------|---------|-----------------| -| horizon-billing-compatibility | **1.1.1** (latest) | Google Play Billing **7.0** API | -| Google Play Billing (Play flavor) | **8.3.0** (latest) | N/A | -| react-native-iap | v14+ | Billing 7.0+, RN 0.79+, Kotlin 2.0+ | -| expo-iap | latest | Billing 7.0+, Kotlin 2.0+ | - -**CRITICAL**: Horizon Billing Compatibility SDK implements Google Play Billing **7.0** API surface, NOT 8.x. - -When writing shared code for both Play and Horizon flavors: -- Use only APIs that exist in **both** Billing 7.0 and 8.x -- Horizon SDK does NOT support Billing 8.x features like auto-reconnect, product status codes, or `includeSuspended` -- OpenIAP handles this automatically with flavor-specific implementations - -### APIs Available in Both (Safe to use in shared code) - -- `BillingClient.Builder`, `BillingClient.newBuilder()` -- `queryProductDetailsAsync()` - Core product query -- `launchBillingFlow()` - Purchase flow -- `acknowledgePurchase()` - Acknowledge (no-op in Horizon) -- `consumeAsync()` - Consume purchase -- `queryPurchasesAsync()` - Query purchases - -### APIs Only in Billing 8.x (DO NOT use in shared code) - -- `enableAutoServiceReconnection()` - Auto-reconnect feature (8.0+) -- Product-level status codes in `queryProductDetailsAsync()` response (8.0+) -- One-time products with multiple offers (8.0+) -- Sub-response codes in `BillingResult` (8.0+) -- `isSuspended` on Purchase (8.1+) -- `includeSuspended` parameter in `QueryPurchasesParams` (8.1+) -- `SubscriptionProductReplacementParams` (8.1+) -- Billing Programs API (`isBillingProgramAvailableAsync`, etc.) (8.2+) -- External Payments / Developer Billing Options (8.3+) - -## Billing Compatibility SDK - -For apps already using Google Play Billing Library, the Horizon Billing Compatibility SDK provides a minimal migration path. - -### Compatibility - -- Compatible with **Google Play Billing Library 7.0** API -- Supports: consumable, durable, and subscription IAP -- Kotlin 2+ required - -### Migration Steps - -Replace imports from: -```kotlin -import com.android.billingclient.api.* -``` - -To: -```kotlin -import com.meta.horizon.billingclient.api.* -``` - -### Key Differences from Google Play Billing - -| Feature | Google Play | Horizon | -|---------|-------------|---------| -| `acknowledgePurchase()` | Required within 3 days | No-op (not required) | -| Non-acknowledgement | Auto-refund after 3 days | No auto-refund | -| `enablePendingPurchases()` | Enables pending purchases | No-op (for compatibility) | -| `onBillingServiceDisconnected()` | Called on disconnect | Never invoked | - -### Important Notes - -- Keep SKUs on Meta Horizon Developer Center same as Google Play Console product IDs -- Only call `consumeAsync()` on consumable items -- `acknowledgePurchase()` is no-op - no acknowledgement requirements - -## Server-to-Server (S2S) APIs - -### Authentication - -Access token format: `OC|App_ID|App_Secret` - -### Verify Entitlement - -Verify that a user owns an item (app or add-on). - -**Endpoint:** - -```http -POST https://graph.oculus.com/$APP_ID/verify_entitlement -``` - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `access_token` | string | `OC\|App_ID\|App_Secret` format | -| `user_id` | string | The user ID to verify | -| `sku` | string | (Optional) SKU for add-on verification | - -**Example - Verify App Ownership:** -```bash -curl -d "access_token=OC|$APP_ID|$APP_SECRET" \ - -d "user_id=$USER_ID" \ - https://graph.oculus.com/$APP_ID/verify_entitlement -``` - -**Example - Verify Add-on/IAP:** -```bash -curl -d "access_token=OC|$APP_ID|$APP_SECRET" \ - -d "user_id=$USER_ID" \ - -d "sku=$SKU" \ - https://graph.oculus.com/$APP_ID/verify_entitlement -``` - -**Response:** -```json -{ - "success": true -} -``` - -### Refund IAP Entitlement - -Refund a DURABLE or CONSUMABLE entitlement (not yet consumed). - -**Endpoint:** - -```http -POST https://graph.oculus.com/$APP_ID/refund_iap_entitlement -``` - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `access_token` | string | `OC\|App_ID\|App_Secret` format | -| `user_id` | string | The user ID | -| `sku` | string | SKU of item to refund | - -**Note:** Can only refund items not yet consumed via `consumeAsync()`. - -## Platform SDK IAP (Native) - -### Product Types - -| Type | Description | -|------|-------------| -| `CONSUMABLE` | Can be purchased multiple times (e.g., coins) | -| `DURABLE` | One-time purchase, permanent ownership | -| `SUBSCRIPTION` | Recurring billing | - -### Key APIs - -#### Get Products - -Retrieve product information and pricing. - -#### Launch Purchase Flow - -Initiate purchase for an item. - -#### Query Purchase History - -Get user's purchase history. - -#### Consume Purchase - -Mark consumable item as used (required for re-purchase). - -## OpenIAP Type Mapping - -| OpenIAP Type | Description | -|--------------|-------------| -| `IapStore.Horizon` | Store identifier for Horizon | -| `VerifyPurchaseHorizonOptions` | Horizon verification parameters | -| `VerifyPurchaseResultHorizon` | Horizon verification result | - -### VerifyPurchaseHorizonOptions - -```typescript -interface VerifyPurchaseHorizonOptions { - userId: string; // Horizon user ID - sku: string; // Product SKU - accessToken: string; // Format: "OC|APP_ID|APP_SECRET" -} -``` - -> **OpenIAP Note**: The GraphQL schema takes a single `accessToken` formatted as `OC|APP_ID|APP_SECRET` rather than separate `appId` / `appSecret` fields. Build the token server-side and pass it as one string. - -### VerifyPurchaseResultHorizon - -```typescript -interface VerifyPurchaseResultHorizon { - success: boolean; // Verification result -} -``` - -## Entitlement Check - -Apps must perform entitlement check within 10 seconds of launch for VRC.Quest.Security.1 compliance. - -## React Native / Expo Support - -Meta Quest supports React Native and Expo applications. - -### Requirements - -| Library | Minimum Version | Notes | -|---------|-----------------|-------| -| react-native-iap | v14+ | Billing 7.0+, Kotlin 2.0+, RN 0.79+ | -| expo-iap | latest | Uses expo-horizon-core plugin | -| React Native | 0.79+ | Required for Nitro modules | -| Kotlin | 2.0+ | Required for both billing SDKs | - -### Expo Integration - -Use `expo-horizon-core` plugin for Quest support: - -```bash -npx expo install expo-horizon-core -``` - -The plugin: -- Removes unsupported dependencies/permissions -- Configures Android product flavors -- Specifies Meta Horizon App ID -- Provides Quest-specific JS utilities - -### Known Limitations on Quest - -- No GPS sensor (limited location accuracy) -- No geocoding support -- No device heading -- No background location -- Some Expo libraries need forks (expo-location, expo-notifications) - -## Documentation Links - -- [Platform SDK IAP Package](https://developers.meta.com/horizon/documentation/android-apps/ps-platform-sdk-iap) -- [S2S APIs](https://developers.meta.com/horizon/documentation/unity/ps-iap-s2s/) -- [Billing Compatibility SDK](https://developers.meta.com/horizon/documentation/spatial-sdk/horizon-billing-compatibility-sdk/) -- [Entitlement Check](https://developers.meta.com/horizon/documentation/android-apps/ps-entitlement-check/) -- [React Native on Quest](https://developers.meta.com/horizon/documentation/android-apps/react-native-apps) -- [Expo Quest Setup](https://blog.swmansion.com/how-to-add-meta-quest-support-to-your-expo-app-68c52778b1fe) -- [Subscriptions](https://developers.meta.com/horizon/resources/subscriptions/) -- [Setting up Add-ons](https://developers.meta.com/horizon/resources/add-ons-setup/) - - ---- - -# react-native-iap API Reference (Legacy) - -> **WARNING**: This file contains outdated API names from older versions. -> For the current API spec, refer to the official [OpenIAP documentation](https://openiap.dev/docs/apis). -> -> Key renames from legacy to current: -> -> - `getProducts` β†’ `fetchProducts` -> - `getSubscriptions` β†’ `fetchProducts({ type: 'subs' })` -> - `getPurchaseHistory` β†’ `getAvailablePurchases` -> - `requestSubscription` β†’ `requestPurchase({ type: 'subs' })` -> - `completePurchase` β†’ `finishTransaction` - -## Overview - -react-native-iap is a React Native library for in-app purchases on iOS and Android. expo-iap is built on top of this library. - -## Installation - -```bash -npm install react-native-iap -# or -yarn add react-native-iap -``` - -## Hook-Based API (Recommended) - -### useIAP Hook - -```typescript -import { useIAP } from 'react-native-iap'; - -function PurchaseScreen() { - const { - connected, - products, - subscriptions, - purchaseHistory, - availablePurchases, - currentPurchase, - currentPurchaseError, - initConnectionError, - finishTransaction, - fetchProducts, - getSubscriptions, - getAvailablePurchases, - getPurchaseHistory, - requestPurchase, - requestSubscription, - } = useIAP(); - - useEffect(() => { - if (currentPurchase) { - // Process purchase - finishTransaction({ purchase: currentPurchase }); - } - }, [currentPurchase]); - - return (/* ... */); -} -``` - -### withIAPContext HOC - -Wrap your app with IAP context provider. - -```typescript -import { withIAPContext } from 'react-native-iap'; - -function App() { - return ; -} - -export default withIAPContext(App); -``` - -## Imperative API - -### Connection Management - -```typescript -import { - initConnection, - endConnection, - fetchProducts, - getSubscriptions, -} from 'react-native-iap'; - -// Initialize -const connected = await initConnection(); - -// Fetch products -const products = await fetchProducts({ skus: ['com.app.product1'] }); -const subs = await getSubscriptions({ skus: ['com.app.sub_monthly'] }); - -// Cleanup -await endConnection(); -``` - -### Product Types - -```typescript -interface Product { - productId: string; - price: string; - currency: string; - localizedPrice: string; - title: string; - description: string; - type: 'inapp' | 'subs'; - - // iOS - introductoryPrice?: string; - introductoryPriceAsAmountIOS?: string; - introductoryPricePaymentModeIOS?: string; - introductoryPriceNumberOfPeriodsIOS?: string; - introductoryPriceSubscriptionPeriodIOS?: string; - subscriptionPeriodNumberIOS?: string; - subscriptionPeriodUnitIOS?: string; - discounts?: Discount[]; - - // Android - subscriptionOfferDetails?: SubscriptionOffer[]; - oneTimePurchaseOfferDetails?: OneTimePurchaseOffer; -} - -interface SubscriptionOffer { - basePlanId: string; - offerId?: string; - offerToken: string; - offerTags: string[]; - pricingPhases: PricingPhase[]; -} - -interface PricingPhase { - formattedPrice: string; - priceCurrencyCode: string; - priceAmountMicros: string; - billingPeriod: string; - billingCycleCount: number; - recurrenceMode: number; -} -``` - -### Purchase Operations - -```typescript -import { - requestPurchase, - requestSubscription, - finishTransaction, - getAvailablePurchases, - getPurchaseHistory, -} from 'react-native-iap'; - -// Purchase consumable/non-consumable -await requestPurchase({ sku: 'com.app.product1' }); - -// Purchase subscription (Android with offer token) -await requestSubscription({ - sku: 'com.app.sub_monthly', - subscriptionOffers: [{ sku: 'com.app.sub_monthly', offerToken: 'token' }], -}); - -// Finish transaction -await finishTransaction({ purchase, isConsumable: true }); - -// Get available purchases (restore) -const available = await getAvailablePurchases(); - -// Get purchase history -const history = await getPurchaseHistory(); -``` - -### Purchase Type - -```typescript -interface Purchase { - productId: string; - transactionId?: string; - transactionDate: number; - transactionReceipt: string; - purchaseToken?: string; - quantityIOS?: number; - originalTransactionDateIOS?: number; - originalTransactionIdentifierIOS?: string; - verificationResultIOS?: string; - appAccountToken?: string; - - // Android - purchaseStateAndroid?: PurchaseStateAndroid; - isAcknowledgedAndroid?: boolean; - packageNameAndroid?: string; - developerPayloadAndroid?: string; - obfuscatedAccountIdAndroid?: string; - obfuscatedProfileIdAndroid?: string; - autoRenewingAndroid?: boolean; -} -``` - -## Event Listeners - -```typescript -import { - purchaseUpdatedListener, - purchaseErrorListener, -} from 'react-native-iap'; - -// Purchase updates -const purchaseUpdateSubscription = purchaseUpdatedListener( - async (purchase: Purchase) => { - const receipt = purchase.transactionReceipt; - if (receipt) { - // Verify with server - await finishTransaction({ purchase }); - } - } -); - -// Purchase errors -const purchaseErrorSubscription = purchaseErrorListener( - (error: PurchaseError) => { - console.warn('purchaseErrorListener', error); - } -); - -// Cleanup -purchaseUpdateSubscription.remove(); -purchaseErrorSubscription.remove(); -``` - -## iOS-Specific Functions - -```typescript -import { - clearTransactionIOS, - clearProductsIOS, - getReceiptIOS, - getPendingPurchasesIOS, - getPromotedProductIOS, - buyPromotedProductIOS, - presentCodeRedemptionSheetIOS, - validateReceiptIos, -} from 'react-native-iap'; - -// Clear finished transactions -await clearTransactionIOS(); - -// Clear cached products -await clearProductsIOS(); - -// Get receipt for validation -const receipt = await getReceiptIOS(); - -// Get pending purchases -const pending = await getPendingPurchasesIOS(); - -// Handle promoted products -const promotedProduct = await getPromotedProductIOS(); -if (promotedProduct) { - await buyPromotedProductIOS(); -} - -// Show offer code redemption -await presentCodeRedemptionSheetIOS(); -``` - -## Android-Specific Functions - -```typescript -import { - acknowledgePurchaseAndroid, - consumePurchaseAndroid, - flushFailedPurchasesCachedAsPendingAndroid, - getPackageNameAndroid, - isFeatureSupported, - getBillingConfigAndroid, -} from 'react-native-iap'; - -// Acknowledge purchase (non-consumables, subscriptions) -await acknowledgePurchaseAndroid({ token: purchase.purchaseToken }); - -// Consume purchase (consumables) -await consumePurchaseAndroid({ token: purchase.purchaseToken }); - -// Clear failed pending purchases -await flushFailedPurchasesCachedAsPendingAndroid(); - -// Get package name -const packageName = getPackageNameAndroid(); - -// Check feature support -const supported = await isFeatureSupported('subscriptions'); - -// Get billing config -const config = await getBillingConfigAndroid(); -``` - -## Subscription Status (iOS) - -```typescript -import { - getSubscriptionStatusIOS, - getSubscriptionStatusesIOS, -} from 'react-native-iap'; - -// Get status for single product -const status = await getSubscriptionStatusIOS('com.app.sub_monthly'); - -// Get status for multiple products -const statuses = await getSubscriptionStatusesIOS(); -``` - -## Error Handling - -```typescript -import { IapIosSk2, ErrorCode } from 'react-native-iap'; - -try { - await requestPurchase({ sku: 'com.app.product1' }); -} catch (err) { - if (err.code === ErrorCode.E_USER_CANCELLED) { - // User cancelled - } else if (err.code === ErrorCode.E_ITEM_UNAVAILABLE) { - // Item not available - } else if (err.code === ErrorCode.E_ALREADY_OWNED) { - // Already owned - } else { - // Other error - } -} -``` - -### Error Codes - -| Code | Description | -|------|-------------| -| `E_UNKNOWN` | Unknown error | -| `E_USER_CANCELLED` | User cancelled | -| `E_ITEM_UNAVAILABLE` | Item not available | -| `E_NETWORK_ERROR` | Network error | -| `E_SERVICE_ERROR` | Store service error | -| `E_ALREADY_OWNED` | Item already owned | -| `E_REMOTE_ERROR` | Remote error | -| `E_NOT_PREPARED` | Not initialized | -| `E_NOT_ENDED` | Not ended | -| `E_DEVELOPER_ERROR` | Developer error | -| `E_BILLING_RESPONSE_JSON_PARSE_ERROR` | JSON parse error | -| `E_DEFERRED_PAYMENT` | Deferred payment | - -## Complete Usage Example - -```typescript -import React, { useEffect } from 'react'; -import { - withIAPContext, - useIAP, - requestPurchase, - finishTransaction, - purchaseUpdatedListener, - purchaseErrorListener, - ProductPurchase, -} from 'react-native-iap'; - -const productIds = ['com.app.product1']; -const subscriptionIds = ['com.app.sub_monthly']; - -function Store() { - const { - connected, - products, - subscriptions, - fetchProducts, - getSubscriptions, - } = useIAP(); - - useEffect(() => { - if (connected) { - fetchProducts({ skus: productIds }); - getSubscriptions({ skus: subscriptionIds }); - } - }, [connected]); - - useEffect(() => { - const purchaseSub = purchaseUpdatedListener( - async (purchase: ProductPurchase) => { - await finishTransaction({ purchase, isConsumable: false }); - } - ); - - const errorSub = purchaseErrorListener((error) => { - console.error('Purchase error:', error); - }); - - return () => { - purchaseSub.remove(); - errorSub.remove(); - }; - }, []); - - const handlePurchase = async (sku: string) => { - try { - await requestPurchase({ sku }); - } catch (err) { - console.error(err); - } - }; - - return (/* Render products and subscriptions */); -} - -export default withIAPContext(Store); -``` - -## Platform Differences - -| Feature | iOS | Android | -|---------|-----|---------| -| Subscription offers | Introductory price, Discounts | Offer tokens, Pricing phases | -| Acknowledge | Automatic | Required within 3 days | -| Consume | finishTransaction | consumePurchaseAndroid | -| Receipt | getReceiptIOS | transactionReceipt in Purchase | -| Promoted products | Supported | Not supported | -| Offer codes | Supported | Promo codes via Play Store | - - ---- - -# StoreKit 2 API Reference - -This document provides external API reference for Apple's StoreKit 2 framework. - -## iOS 18+ Features - -| Feature | iOS Version | Description | -|---------|-------------|-------------| -| Win-back offers | iOS 18.0 | Re-engage churned subscribers | -| `eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase | -| Consumable transaction history | iOS 18.0 | Opt-in via `SK2ConsumableTransactionHistory` Info.plist key | -| StoreKit `Message` API | iOS 18.0 | Listener for billing issues, win-back, price increase, generic | -| UI context for purchases | iOS 18.2 | Required for proper payment sheet display | -| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` | -| `appTransactionID` | iOS 18.4 | Globally unique app transaction identifier (back-deployed to iOS 15) | -| `originalPlatform` | iOS 18.4 | Original purchase platform (back-deployed to iOS 15) | -| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction | -| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction | -| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction | -| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables | -| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | -| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | -| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` | - -### WWDC 2025 Updates - -- **SubscriptionStatus by Transaction ID**: `SubscriptionInfo.Status.status(for: transactionID:)` accepts any transaction ID, not just SKU. -- **JWS-based promotional offers**: New `promotionalOffer` purchase option with compact JWS string. -- **Introductory offer eligibility**: Override eligibility check with `introductoryOfferEligibility` purchase option. -- Both new purchase options are back-deployed to iOS 15. - -## appAccountToken - -A UUID that associates a purchase with a user account in your system. This property allows you to correlate App Store transactions with users in your backend. - -### Important: UUID Format Requirement - -**The `appAccountToken` must be a valid UUID format.** If you provide a non-UUID string (e.g., `"user-123"` or `"my-account-id"`), Apple's StoreKit will silently return `null` for this field in the transaction response. - -#### Valid UUID Examples - -```swift -// Valid UUIDs - these will be returned correctly -"550e8400-e29b-41d4-a716-446655440000" -"6ba7b810-9dad-11d1-80b4-00c04fd430c8" -UUID().uuidString // Generate new UUID -``` - -#### Invalid Examples (Will Return null) - -```swift -// Invalid - NOT UUID format, Apple returns null silently -"user-123" -"my-account-token" -"abc123" -``` - -### Usage in Purchase Options - -```swift -let appAccountToken = UUID() -let result = try await product.purchase(options: [ - .appAccountToken(appAccountToken) -]) -``` - -### Retrieving from Transaction - -```swift -let transaction: Transaction -if let token = transaction.appAccountToken { - // Token will only be present if a valid UUID was provided during purchase - print("App Account Token: \(token)") -} -``` - -### Best Practices - -1. **Generate UUIDs per user**: Create and store a UUID for each user in your system -2. **Use consistent tokens**: Use the same UUID for all purchases from the same user -3. **Server-side mapping**: Map the UUID to your internal user ID on your server -4. **Don't use user IDs directly**: Convert your user IDs to UUIDs rather than using them directly - -### References - -- [Apple Developer Documentation: appAccountToken](https://developer.apple.com/documentation/storekit/transaction/appaccounttoken) -- [GitHub Issue: expo-iap #128](https://github.com/hyochan/expo-iap/issues/128) - -## Product - -A type that describes an in-app purchase product. - -### Properties - -```swift -let id: String // The product identifier -let type: Product.ProductType // The type of product -let displayName: String // Localized display name -let description: String // Localized description -let displayPrice: String // Localized price string -let price: Decimal // Price as decimal -let subscription: Product.SubscriptionInfo? // Subscription details -``` - -### Methods - -#### products(for:) - -```swift -static func products(for identifiers: [String]) async throws -> [Product] -``` - -Fetches products from the App Store. - -#### purchase(options:) - -```swift -func purchase(options: Set = []) async throws -> Product.PurchaseResult -``` - -Initiates a purchase for this product. - -## Transaction - -Represents a completed purchase transaction. - -### Properties - -```swift -let id: UInt64 // Unique transaction ID -let originalID: UInt64 // Original transaction ID -let productID: String // Product identifier -let purchaseDate: Date // When the purchase occurred -let expirationDate: Date? // Subscription expiration date -let revocationDate: Date? // When the transaction was revoked -let isUpgraded: Bool // Whether this subscription was upgraded -let environment: AppStore.Environment // sandbox or production -``` - -### Methods - -#### currentEntitlements - -```swift -static var currentEntitlements: Transaction.Entitlements -``` - -A sequence of the customer's current entitlements. - -#### latest(for:) - -```swift -static func latest(for productID: String) async -> VerificationResult? -``` - -Gets the latest transaction for a product. - -#### finish() - -```swift -func finish() async -``` - -Marks the transaction as finished. - -## AppStore - -Provides access to App Store functionality. - -### Methods - -#### sync() - -```swift -static func sync() async throws -``` - -Syncs transactions with the App Store. - -#### showManageSubscriptions(in:) - -```swift -static func showManageSubscriptions(in scene: UIWindowScene) async throws -``` - -Shows the subscription management UI. - -#### beginRefundRequest(for:in:) - -```swift -static func beginRefundRequest(for transactionID: UInt64, in scene: UIWindowScene) async throws -> Transaction.RefundRequestStatus -``` - -Begins a refund request for a transaction. - -## Win-Back Offers (iOS 18+) - -Win-back offers are a new offer type to re-engage churned subscribers. - -### Automatic Presentation - -StoreKit Message automatically presents win-back offers when a user is eligible: - -```swift -// Message reason for win-back offers -StoreKit.Message.Reason.winBackOffer -``` - -### Manual Application - -Apply a win-back offer during purchase: - -```swift -let product: Product -let winBackOffer: Product.SubscriptionOffer - -let result = try await product.purchase(options: [ - .winBackOffer(winBackOffer) -]) -``` - -### Checking Eligibility - -Discover eligible win-back offers before purchase via `Product.SubscriptionInfo.eligibleWinBackOfferIDs` (iOS 18+): - -```swift -let status = try await product.subscription?.status.first -guard let renewalInfo = try status?.renewalInfo.payloadValue else { return } - -// iOS 18+: offer IDs the current Apple Account is eligible for -let eligibleIDs = renewalInfo.eligibleWinBackOfferIDs -let eligibleOffers = (product.subscription?.promotionalOffers ?? []).filter { - $0.type == .winBack && eligibleIDs.contains($0.id ?? "") -} -``` - -### RenewalInfo - -Win-back offer information is available in renewal info: - -```swift -let renewalInfo: Product.SubscriptionInfo.RenewalInfo - -// Check if win-back offer is applied to next renewal -if renewalInfo.renewalOfferType == .winBack { - // Win-back offer will be applied -} -``` - -## UI Context for Purchases (iOS 18.2+) - -Beginning in iOS 18.2, purchase methods require a UI context to properly display payment sheets: - -```swift -// iOS/iPadOS/tvOS/visionOS: UIViewController -let result = try await product.purchase(confirmIn: viewController) - -// macOS: NSWindow -let result = try await product.purchase(confirmIn: window) - -// watchOS: No UI context required -``` - -> **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. - -## AppTransaction Updates (iOS 18.4+) - -```swift -let appTransaction = try await AppTransaction.shared - -// New in iOS 18.4 (back-deployed to iOS 15) -let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account -let originalPlatform = appTransaction.originalPlatform // Original purchase platform -``` - -### appTransactionID - -- Globally unique identifier for each Apple Account that downloads your app -- Remains consistent across redownloads, refunds, repurchases, and storefront changes -- Works with Family Sharing (each family member gets unique ID) -- Back-deployed to iOS 15 - -## Transaction Updates (iOS 18.4+) - -iOS 18.4 added three new read-only properties to `Transaction` (not just `AppTransaction`): - -```swift -let transaction: Transaction - -// iOS 18.4+ β€” all back-deployed to iOS 15 -let txAppTransactionID = transaction.appTransactionID // Apple Account identifier -let offerPeriod = transaction.offerPeriod // Offer.Period? -let advancedCommerce = transaction.advancedCommerceInfo // AdvancedCommerceInfo? -``` - -| Property | Type | Notes | -|----------|------|-------| -| `appTransactionID` | String | Mirrors AppTransaction's identifier | -| `offerPeriod` | Offer.Period? | Phase of the promotional/intro offer | -| `advancedCommerceInfo` | AdvancedCommerceInfo? | Present for Advanced Commerce SKUs only | - -## Advanced Commerce API (iOS 18.4+) - -For apps with large product catalogs: - -```swift -// Check if product has advanced commerce info -if let advancedInfo = product.advancedCommerceInfo { - // Handle large catalog monetization -} -``` - -## StoreKit Message API (iOS 18+) - -Listen for App Store–generated messages (billing issues, win-back offers, price increases, generic). - -```swift -// Somewhere near app launch -Task { - for await message in Message.messages { - switch message.reason { - case .billingIssue: - // Show UI when user is ready; display from message.display(in:) - break - case .winBackOffer: - break - case .priceIncrease: - break - case .generic: - break - @unknown default: - break - } - } -} -``` - -| Reason | Trigger | -|--------|---------| -| `.billingIssue` | User has an unresolved billing problem on a subscription | -| `.priceIncrease` | Price change that requires user consent | -| `.winBackOffer` | User is eligible for a win-back offer | -| `.generic` | All other system-initiated messages | - -> **OpenIAP Note**: To be surfaced by the cross-platform event layer β€” see `event.graphql` additions for message events. - -## SubscriptionStatus by Transaction ID (WWDC 2025) - -```swift -// WWDC 2025: look up status using any transactionID, not just a SKU -let status = try await Product.SubscriptionInfo.Status.status(for: transactionID) -``` - -## Consumable Transaction History (iOS 18+) - -By default, `Transaction.all` omits finished consumables. Opt in by adding this key to **Info.plist**: - -```xml -SK2ConsumableTransactionHistory - -``` - -With the key set, finished consumable transactions are included in `Transaction.all` and `Transaction.currentEntitlements`. - -## External Purchase Support (iOS 17.4+) - -`ExternalPurchase.presentNoticeSheet()` / `ExternalPurchase.open(url:)` ship on iOS 17.4+. The follow-on custom-link APIs (`ExternalPurchaseCustomLink.isEligible`, `showNotice(type:)`, `token(for:)`) are iOS 18.1+. - -### Present External Purchase Notice - -```swift -// Check if external purchase notice can be presented -if await ExternalPurchase.canPresent { - let result = try await ExternalPurchase.presentNoticeSheet() - switch result { - case .continue: - // User wants to continue to external purchase - case .dismissed: - // User dismissed the notice - } -} -``` - -### Present External Purchase Link - -```swift -let result = try await ExternalPurchase.open(url: externalURL) -``` - -> **OpenIAP Note**: `presentExternalPurchaseNoticeSheetIOS` and `presentExternalPurchaseLinkIOS` are available in the iOS package. - - ---- - -# Webhook Event Mapping (ASN v2 ↔ RTDN ↔ openiap) - -This document is the source of truth for how kit normalizes Apple App Store Server -Notifications v2 (ASN v2) and Google Play Real-Time Developer Notifications (RTDN) -into the unified `WebhookEvent` shape defined in [`packages/gql/src/webhook.graphql`](../../packages/gql/src/webhook.graphql). - -When kit's webhook receivers are implemented (Phase 1, PR #2), they MUST follow -this table. When extending the spec (new event types, new stores), update this -document in the same PR. - -## Subscription lifecycle - -| openiap `WebhookEventType` | Apple ASN v2 `notificationType` (`subtype`) | Google RTDN `subscriptionNotification.notificationType` | -|---|---|---| -| `SubscriptionStarted` | `SUBSCRIBED` (`INITIAL_BUY`, `RESUBSCRIBE`) | `SUBSCRIPTION_PURCHASED` (4) | -| `SubscriptionRenewed` | `DID_RENEW` | `SUBSCRIPTION_RENEWED` (2) | -| `SubscriptionExpired` | `EXPIRED` | `SUBSCRIPTION_EXPIRED` (13) | -| `SubscriptionInGracePeriod` | `DID_FAIL_TO_RENEW` (`GRACE_PERIOD`) | `SUBSCRIPTION_IN_GRACE_PERIOD` (6) | -| `SubscriptionInBillingRetry` | `DID_FAIL_TO_RENEW` (no subtype) | `SUBSCRIPTION_ON_HOLD` (5) | -| `SubscriptionRecovered` | `DID_RENEW` (after a prior failure) | `SUBSCRIPTION_RECOVERED` (1) | -| `SubscriptionCanceled` | `DID_CHANGE_RENEWAL_STATUS` (`AUTO_RENEW_DISABLED`) | `SUBSCRIPTION_CANCELED` (3) | -| `SubscriptionUncanceled` | `DID_CHANGE_RENEWAL_STATUS` (`AUTO_RENEW_ENABLED`) | `SUBSCRIPTION_RESTARTED` (7) β€” fired when auto-renew is re-enabled while the period is still active | -| `SubscriptionRevoked` | `REVOKE` | `SUBSCRIPTION_REVOKED` (12) | -| `SubscriptionPriceChange` | `PRICE_INCREASE` | `SUBSCRIPTION_PRICE_CHANGE_CONFIRMED` (8), `SUBSCRIPTION_PRICE_CHANGE_UPDATED` (19) | -| `SubscriptionProductChanged` | `DID_CHANGE_RENEWAL_PREF` | `SUBSCRIPTION_DEFERRED` (9) | -| `SubscriptionPaused` | (no equivalent β€” iOS has no pause) | `SUBSCRIPTION_PAUSED` (10), `SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED` (11) β€” schedule update, not actual resume | -| `SubscriptionResumed` | (no equivalent) | `SUBSCRIPTION_RECOVERED` (1) when fired after a `SUBSCRIPTION_PAUSED` β€” kit chooses Resumed vs Recovered based on the prior `subscriptions` row state | - -PR #123 review caught the earlier draft where codes 1 and 4 were swapped -(`SUBSCRIPTION_RECOVERED` is code 1, `SUBSCRIPTION_PURCHASED` is code 4) -and where `SUBSCRIPTION_RESTARTED` (7) was incorrectly mapped to -`SubscriptionRecovered` instead of `SubscriptionUncanceled`. The mapping -above reflects the corrected RTDN reference. - -## One-time / common - -| openiap `WebhookEventType` | Apple ASN v2 | Google RTDN | -|---|---|---| -| `PurchaseRefunded` | `REFUND` | `oneTimeProductNotification.notificationType = ONE_TIME_PRODUCT_CANCELED` (2), or `voidedPurchaseNotification` | -| `PurchaseConsumptionRequest` | `CONSUMPTION_REQUEST` | (no equivalent β€” Play handles consumption client-side) | -| `TestNotification` | `TEST` | `testNotification` field present on the RTDN message | - -## Field mapping - -| `WebhookEvent` field | Apple ASN v2 source | Google RTDN source | -|---|---|---| -| `id` | `notificationUUID` | Pub/Sub `messageId` | -| `occurredAt` | `signedDate` | `eventTimeMillis` | -| `environment` | `data.environment` (`Production` \| `Sandbox` \| `Xcode`) | `testNotification` present β†’ `Sandbox`, else `Production` | -| `purchaseToken` | `data.signedTransactionInfo.originalTransactionId` | `subscriptionNotification.purchaseToken` or `oneTimeProductNotification.purchaseToken` | -| `productId` | `data.signedTransactionInfo.productId` | `subscriptionNotification.subscriptionId` or `oneTimeProductNotification.sku` | -| `expiresAt` | `data.signedRenewalInfo.expirationDate` (decoded JWS) | resolved by calling `purchases.subscriptionsv2.get` (ASN/RTDN do not embed it directly) | -| `renewsAt` | `data.signedRenewalInfo.renewalDate` | resolved by calling `purchases.subscriptionsv2.get` | -| `cancellationReason` | `data.signedTransactionInfo.revocationReason` + ASN `subtype` | `purchases.subscriptionsv2.get` β†’ `canceledStateContext.userInitiatedCancellation` / `systemInitiatedCancellation` | -| `currency` | `data.signedTransactionInfo.currency` | from `purchases.subscriptionsv2.get` linked product price | -| `priceAmountMicros` | `data.signedTransactionInfo.price` Γ— 1000 (Apple's `price` field is in **milliunits** = 1/1000 of a currency unit; multiply by 1000 to convert to micros) | `purchases.subscriptionsv2.get` β†’ `lineItems[*].autoRenewingPlan.recurringPrice` β€” `units * 1_000_000 + Math.round(nanos / 1000)` (Money type combines whole units + nanos = 10⁻⁹ units) | -| `rawSignedPayload` | The complete `signedPayload` JWS string from the ASN body | The base64-decoded Pub/Sub message `data` (JSON) | - -## Validation requirements (kit Phase 1, PR #2) - -Both stores require signature verification before any event is emitted: - -- **Apple ASN v2**: verify the JWS using Apple's public root certificates (refresh - via the App Store Connect API). The receiver must reject unverified payloads - with HTTP 401. -- **Google RTDN**: validate the Pub/Sub push request against the configured - service account audience (OIDC token verification). Reject missing or invalid - tokens with HTTP 401. - -Idempotency: - -- Use `(source, sourceNotificationId)` as the dedup key, where - `sourceNotificationId` is `notificationUUID` for ASN v2 or `messageId` for - RTDN. Convex idempotency table records the first-seen event and silently - acknowledges duplicates with HTTP 200. - -Replay window: - -- Events MUST be retained for at least 30 days so `webhookEventsSince` can - service reconnecting clients. Older events are pruned by a Convex cron job. - - ---- - -## Links & Resources - -- Documentation: https://openiap.dev/docs -- Types Reference: https://openiap.dev/docs/types -- APIs Reference: https://openiap.dev/docs/apis -- Error Codes: https://openiap.dev/docs/errors -- GitHub: https://github.com/hyodotdev/openiap - -### Ecosystem Libraries -- expo-iap: https://github.com/hyodotdev/openiap/tree/main/libraries/expo-iap -- react-native-iap: https://github.com/hyodotdev/openiap/tree/main/libraries/react-native-iap -- flutter_inapp_purchase: https://github.com/hyodotdev/openiap/tree/main/libraries/flutter_inapp_purchase -- godot-iap: https://github.com/hyodotdev/openiap/tree/main/libraries/godot-iap -- kmp-iap: https://github.com/hyodotdev/openiap/tree/main/libraries/kmp-iap -- maui-iap: https://github.com/hyodotdev/openiap/tree/main/libraries/maui-iap diff --git a/llms-full.txt b/llms-full.txt new file mode 120000 index 000000000..7ae46db64 --- /dev/null +++ b/llms-full.txt @@ -0,0 +1 @@ +packages/docs/public/llms-full.txt \ No newline at end of file diff --git a/llms.txt b/llms.txt deleted file mode 100644 index 7194caf9c..000000000 --- a/llms.txt +++ /dev/null @@ -1,231 +0,0 @@ -# OpenIAP Quick Reference - -> OpenIAP: Unified in-app purchase specification for iOS & Android -> Documentation: https://openiap.dev -> Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-06-15T14:57:16.350Z - -## Installation - -### React Native / Expo -```bash -# expo-iap (Expo projects) -npx expo install expo-iap - -# react-native-iap (React Native CLI) -npm install react-native-iap -``` - -### Native -```swift -// Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") -``` - -```kotlin -// Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.2.1") -``` - -```bash -# Flutter -flutter pub add flutter_inapp_purchase -``` - -```gdscript -# Godot -# Install godot-iap 2.3.1 to addons/godot-iap and enable the plugin -``` - -```kotlin -// Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.3.1") -``` - -```bash -# .NET MAUI -dotnet add package OpenIap.Maui -``` - -Current NuGet package version: 1.1.1 - -## Framework Libraries - -- `expo-iap`: Expo Modules wrapper, same OpenIAP API as React Native. -- `react-native-iap`: Nitro Modules wrapper for React Native CLI apps. -- `flutter_inapp_purchase`: Dart API with generated OpenIAP types and streams. -- `godot-iap`: Godot 4.x plugin with GDScript functions and signals. -- `kmp-iap`: Kotlin Multiplatform API with Flow-based purchase events. -- `maui-iap`: `OpenIap.Maui` package with `OpenIapClient.Instance`, - generated `Types.cs`, IAPKit helpers (`OpenIapClient.KitApi`, - `OpenIapClient.ConnectWebhookStream`, - `OpenIapClient.ParseWebhookEventData`), flattened OpenIAP-owned iOS - xcframework / Android AAR bindings, Google and AndroidX Android - dependencies as NuGet package references, and MAUI example flows matching - `expo-iap`. - -## Core APIs - -### Connection -```typescript -// Initialize (required before any operation) -await initConnection(); - -// With alternative billing (Android) -await initConnection({ alternativeBillingModeAndroid: 'user-choice' }); - -// Cleanup on unmount -await endConnection(); -``` - -### Fetch Products -```typescript -const products = await fetchProducts({ - products: [ - { id: 'com.app.premium', type: 'inapp' }, - { id: 'com.app.monthly', type: 'subs' }, - ], -}); -``` - -### Request Purchase -```typescript -// IMPORTANT: requestPurchase is event-based, not promise-based -// Set up purchaseUpdatedListener before calling -await requestPurchase({ - request: { - apple: { sku: 'com.app.premium' }, - google: { skus: ['com.app.premium'] }, - }, - type: 'inapp', // 'inapp' | 'subs' -}); -``` - -### Finish Transaction -```typescript -// CRITICAL: Must call after verification -// Android: purchases auto-refund after 3 days if not acknowledged -await finishTransaction(purchase, isConsumable); -``` - -### Get Available Purchases -```typescript -const purchases = await getAvailablePurchases(); -// Returns user's current entitlements -``` - -### Restore Purchases -```typescript -await restorePurchases(); -const purchases = await getAvailablePurchases(); -``` - -## Events (React Native/Expo) - -```typescript -import { purchaseUpdatedListener, purchaseErrorListener } from 'expo-iap'; - -// Set up before any purchase request -const purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase) => { - // 1. Verify purchase on server - // 2. Grant entitlement - // 3. Finish transaction - await finishTransaction(purchase); -}); - -const purchaseErrorSubscription = purchaseErrorListener((error) => { - if (error.code === 'UserCancelled') return; // Normal flow - console.error('Purchase error:', error.message); -}); - -// Cleanup -purchaseUpdateSubscription.remove(); -purchaseErrorSubscription.remove(); -``` - -## Core Types - -### Product -```typescript -interface Product { - id: string; // Product identifier (SKU) - title: string; // Display name - description: string; // Product description - price: string; // Formatted price string - priceAmount: number; // Price as number - currency: string; // ISO 4217 currency code - type: 'inapp' | 'subs'; -} -``` - -### Purchase -```typescript -interface Purchase { - productId: string; // Purchased product ID - transactionId: string; // Platform transaction ID - transactionDate: number; // Purchase timestamp - purchaseState: PurchaseState; - // iOS specific - originalTransactionId?: string; - // Android specific - purchaseToken?: string; - orderId?: string; -} - -type PurchaseState = 'purchased' | 'pending' | 'restored'; -``` - -### PurchaseError -```typescript -interface PurchaseError { - code: string; // Error code - message: string; // Human-readable message - productId?: string; // Related SKU -} -``` - -## Common Error Codes - -| Code | Description | Action | -|------|-------------|--------| -| UserCancelled | User cancelled purchase | No action needed | -| ItemUnavailable | Product not in store | Check store config | -| AlreadyOwned | Already purchased | Restore purchases | -| NetworkError | Network issue | Retry with backoff | -| ServiceError | Store service error | Retry later | -| NotPrepared | initConnection not called | Call initConnection first | - -## API Naming Convention - -- **Cross-platform**: No suffix (fetchProducts, requestPurchase) -- **iOS-only**: `IOS` suffix (syncIOS, getStorefrontIOS) -- **Android-only**: `Android` suffix (acknowledgePurchaseAndroid) - -## Platform-Specific APIs - -### iOS -- syncIOS() - Sync with App Store -- presentCodeRedemptionSheetIOS() - Show offer code UI -- showManageSubscriptionsIOS() - Open subscription management -- beginRefundRequestIOS() - Start refund flow - -### Android -- acknowledgePurchaseAndroid() - Acknowledge purchase -- consumePurchaseAndroid() - Consume for re-purchase - -## Purchase Flow Summary - -1. initConnection() -2. fetchProducts([...skus]) -3. Set up purchaseUpdatedListener -4. requestPurchase({ sku }) -5. In listener: verify -> grant -> finishTransaction() -6. endConnection() on cleanup - -## Links - -- Docs: https://openiap.dev/docs -- Types: https://openiap.dev/docs/types -- APIs: https://openiap.dev/docs/apis -- Errors: https://openiap.dev/docs/errors -- GitHub: https://github.com/hyodotdev/openiap diff --git a/llms.txt b/llms.txt new file mode 120000 index 000000000..8099ba3ee --- /dev/null +++ b/llms.txt @@ -0,0 +1 @@ +packages/docs/public/llms.txt \ No newline at end of file diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index 86d8fb5c1..dd3f18fd6 100644 --- a/packages/docs/public/llms-full.txt +++ b/packages/docs/public/llms-full.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-06-15T14:57:16.350Z +> Generated: 2026-06-23T15:25:38.051Z ## Table of Contents 1. Installation @@ -39,10 +39,10 @@ pod 'openiap', '~> 2.2.1' ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.2.1") +implementation("io.github.hyochan.openiap:openiap-google:2.2.2") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.1") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.2") ``` ### Flutter @@ -51,13 +51,13 @@ flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap-2.3.1.zip` from GitHub Releases, extract it to +Download `godot-iap-2.3.2.zip` from GitHub Releases, extract it to `addons/godot-iap/`, then enable the plugin in Project Settings. ### Kotlin Multiplatform ```kotlin dependencies { - implementation("io.github.hyochan:kmp-iap:2.3.1") + implementation("io.github.hyochan:kmp-iap:2.3.2") } ``` @@ -69,7 +69,7 @@ https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.1.1 +Current NuGet package version: 1.1.5 Requires .NET 9 or .NET 10, the MAUI workload, iOS 15.0+, and Android API 24+. diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index 7194caf9c..bd86e01fa 100644 --- a/packages/docs/public/llms.txt +++ b/packages/docs/public/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-06-15T14:57:16.350Z +> Generated: 2026-06-23T15:25:38.051Z ## Installation @@ -24,7 +24,7 @@ npm install react-native-iap ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.2.1") +implementation("io.github.hyochan.openiap:openiap-google:2.2.2") ``` ```bash @@ -34,12 +34,12 @@ flutter pub add flutter_inapp_purchase ```gdscript # Godot -# Install godot-iap 2.3.1 to addons/godot-iap and enable the plugin +# Install godot-iap 2.3.2 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.3.1") +implementation("io.github.hyochan:kmp-iap:2.3.2") ``` ```bash @@ -47,7 +47,7 @@ implementation("io.github.hyochan:kmp-iap:2.3.1") dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.1.1 +Current NuGet package version: 1.1.5 ## Framework Libraries diff --git a/packages/docs/src/pages/docs/setup/flutter.tsx b/packages/docs/src/pages/docs/setup/flutter.tsx index 6f4da4386..49e900fa9 100644 --- a/packages/docs/src/pages/docs/setup/flutter.tsx +++ b/packages/docs/src/pages/docs/setup/flutter.tsx @@ -14,7 +14,7 @@ function FlutterSetup() {

Flutter Setup

flutter_inapp_purchase provides in-app purchase support for - Flutter apps on iOS and Android. + Flutter apps on iOS, Android, and macOS.

+

+ iOS/macOS Native Dependency Manager + + # + +

+

+ The Dart install command above is the only package install step for + most apps. On Flutter 3.44 and newer, Swift Package Manager is enabled + by default, and the Flutter CLI resolves the native OpenIAP dependency + automatically when you run or build the app. +

+

+ Projects that disable Swift Package Manager, or projects using an + older Flutter toolchain, continue to use CocoaPods. In that case, run + CocoaPods after flutter pub get: +

+ + {`(cd ios && pod install) + +# If your app also has a macOS target: +(cd macos && pod install)`} + +

+ Do not add OpenIAP manually to your app's{' '} + Package.swift or Podfile; the Flutter plugin + declares the native dependency. +

+

iOS Configuration @@ -77,6 +106,23 @@ function FlutterSetup() { `} +

+ macOS Configuration + + # + +

+
    +
  • + Requires macOS 14.0+ +
  • +
  • + Enable In-App Purchase capability in Xcode: Target >{' '} + Signing & Capabilities >{' '} + + Capability > In-App Purchase +
  • +
+

Android Configuration diff --git a/scripts/agent/README.md b/scripts/agent/README.md index e7eba02f1..9c992db39 100644 --- a/scripts/agent/README.md +++ b/scripts/agent/README.md @@ -109,7 +109,7 @@ cp .env.example .env ```bash # Compile for AI assistants (no Ollama required) bun run compile:ai -# β†’ generates context.md, llms.txt, llms-full.txt +# β†’ generates context.md and docs public llms files; root llms files are symlinks # Compile for Local RAG (Ollama required) bun run compile:local @@ -212,7 +212,7 @@ openiap/ | Script | Description | Ollama Required | |--------|-------------|-----------------| | `bun run compile` | Compile all (AI context + Local RAG) | Yes | -| `bun run compile:ai` | Generate context.md, llms.txt, llms-full.txt | No | +| `bun run compile:ai` | Generate context.md and docs public llms files | No | | `bun run compile:local` | Index knowledge + code map (LanceDB) | Yes | | `bun run compile:local:knowledge` | Index only knowledge files | Yes | | `bun run compile:local:code` | Build only code map | Yes | @@ -398,5 +398,5 @@ bun run compile:ai ```bash bun run compile:ai -# β†’ regenerates packages/docs/public/llms.txt +# β†’ regenerates packages/docs/public/llms*.txt and refreshes root symlinks ``` diff --git a/scripts/agent/compile-context.ts b/scripts/agent/compile-context.ts index 7e8d37b38..5b44db5b6 100644 --- a/scripts/agent/compile-context.ts +++ b/scripts/agent/compile-context.ts @@ -35,7 +35,10 @@ const CONFIG = { outputFile: "context.md", // LLMs.txt output (for AI assistants on web) llmsOutputDir: path.resolve(scriptDir, "../../packages/docs/public"), - rootLlmsOutputDir: path.resolve(scriptDir, "../.."), + rootLlmsSymlinks: { + "llms.txt": "packages/docs/public/llms.txt", + "llms-full.txt": "packages/docs/public/llms-full.txt", + }, }; function readInstallationVersions(): { @@ -89,6 +92,26 @@ function withSingleTrailingNewline(content: string): string { return `${content.trimEnd()}\n`; } +function ensureSymlink(linkPath: string, targetPath: string): void { + try { + const currentTarget = fs.readlinkSync(linkPath); + if (currentTarget === targetPath) { + return; + } + fs.unlinkSync(linkPath); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ENOENT" && code !== "EINVAL") { + throw error; + } + if (fs.existsSync(linkPath)) { + fs.unlinkSync(linkPath); + } + } + + fs.symlinkSync(targetPath, linkPath); +} + // ============================================================================ // LLMs.txt Generator // ============================================================================ @@ -583,20 +606,19 @@ interface PurchaseError { - GitHub: https://github.com/hyodotdev/openiap `; - // Write files to the deployed docs public directory and the repository root. - // The website serves packages/docs/public, while the root copies keep local - // repository consumers from reading stale LLM context. - const outputDirs = [CONFIG.llmsOutputDir, CONFIG.rootLlmsOutputDir]; - for (const outputDir of outputDirs) { - fs.mkdirSync(outputDir, { recursive: true }); - fs.writeFileSync( - path.join(outputDir, "llms.txt"), - withSingleTrailingNewline(quickContent), - ); - fs.writeFileSync( - path.join(outputDir, "llms-full.txt"), - withSingleTrailingNewline(fullContent), - ); + // The website serves packages/docs/public. Root files are symlinks to avoid + // drift between local repository readers and deployed docs. + fs.mkdirSync(CONFIG.llmsOutputDir, { recursive: true }); + fs.writeFileSync( + path.join(CONFIG.llmsOutputDir, "llms.txt"), + withSingleTrailingNewline(quickContent), + ); + fs.writeFileSync( + path.join(CONFIG.llmsOutputDir, "llms-full.txt"), + withSingleTrailingNewline(fullContent), + ); + for (const [filename, targetPath] of Object.entries(CONFIG.rootLlmsSymlinks)) { + ensureSymlink(path.join(CONFIG.projectRoot, filename), targetPath); } console.log( @@ -781,14 +803,9 @@ openiap/ ` βœ“ Output: ${path.join(CONFIG.llmsOutputDir, "llms-full.txt")}`, ), ); - console.log( - chalk.green(` βœ“ Output: ${path.join(CONFIG.rootLlmsOutputDir, "llms.txt")}`), - ); - console.log( - chalk.green( - ` βœ“ Output: ${path.join(CONFIG.rootLlmsOutputDir, "llms-full.txt")}`, - ), - ); + for (const [filename, targetPath] of Object.entries(CONFIG.rootLlmsSymlinks)) { + console.log(chalk.green(` βœ“ Symlink: ${filename} -> ${targetPath}`)); + } console.log(chalk.bold.green("\nβœ… Context compilation complete!\n")); console.log(chalk.white("Usage with Claude Code:")); diff --git a/scripts/audit-non-godot-parity.mjs b/scripts/audit-non-godot-parity.mjs index b74a45d17..f282c5e61 100644 --- a/scripts/audit-non-godot-parity.mjs +++ b/scripts/audit-non-godot-parity.mjs @@ -574,11 +574,45 @@ function checkFlutter() { "invokeMethod('deepLinkToSubscriptions')", 'deepLinkToSubscriptionsAndroid', ], 'Flutter deepLinkToSubscriptions bridge'); - expectIncludes('libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift', [ + const flutterIosPlugin = + 'libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift'; + const flutterMacosPlugin = + 'libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift'; + const flutterSwiftPackagePaths = [ + 'libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Package.swift', + 'libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Package.swift', + ]; + + for (const flutterSwiftPackage of flutterSwiftPackagePaths) { + expectFile(flutterSwiftPackage); + if (!exists(flutterSwiftPackage)) continue; + const flutterSwiftPackageText = read(flutterSwiftPackage); + const openIapDependencyVersion = flutterSwiftPackageText.match( + /\.package\(url: "https:\/\/github\.com\/hyodotdev\/openiap\.git", from: "([^"]+)"\),/, + )?.[1]; + if (!openIapDependencyVersion) { + fail(`${flutterSwiftPackage} is missing the OpenIAP SwiftPM dependency`); + } else if ( + !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test( + openIapDependencyVersion, + ) + ) { + fail(`${flutterSwiftPackage} OpenIAP dependency version must be semver`); + } + if ( + !flutterSwiftPackageText.includes( + '.product(name: "OpenIAP", package: "OpenIAP")', + ) + ) { + fail(`${flutterSwiftPackage} is missing the OpenIAP product dependency`); + } + } + + expectIncludes(flutterIosPlugin, [ 'case "deepLinkToSubscriptions"', 'OpenIapModule.shared.deepLinkToSubscriptions(nil)', ], 'Flutter iOS deepLinkToSubscriptions bridge'); - expectIncludes('libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift', [ + expectIncludes(flutterMacosPlugin, [ 'case "setPurchaseUpdatedListenerOptions"', 'case "deepLinkToSubscriptions"', 'case "getAllTransactionsIOS"', @@ -599,18 +633,18 @@ function checkFlutter() { ], 'Flutter Android must inherit Play Billing from openiap-google'); for (const nativePlugin of [ 'libraries/expo-iap/ios/ExpoIapModule.swift', - 'libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift', - 'libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift', + flutterIosPlugin, + flutterMacosPlugin, ]) { expectNotIncludes(nativePlugin, [ 'OpenIapModule.shared.validateReceiptIOS', 'OpenIapModule.shared.getStorefrontIOS()', ], 'Flutter deprecated native OpenIAP calls'); } - expectIncludes('libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift', [ + expectIncludes(flutterIosPlugin, [ 'OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()', ], 'Flutter iOS promoted purchase bridge'); - expectIncludes('libraries/flutter_inapp_purchase/macos/Classes/FlutterInappPurchasePlugin.swift', [ + expectIncludes(flutterMacosPlugin, [ 'OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()', ], 'Flutter macOS promoted purchase bridge'); expectIncludes('libraries/react-native-iap/ios/HybridRnIap.swift', [ @@ -981,6 +1015,25 @@ function checkFrameworkDependencyHygiene() { 'packages/docs/openiap-versions.json', 'Docs package version copy', ); + expectSymlinkTarget( + 'llms.txt', + 'packages/docs/public/llms.txt', + 'Root llms.txt', + ); + expectSymlinkTarget( + 'llms-full.txt', + 'packages/docs/public/llms-full.txt', + 'Root llms-full.txt', + ); + for (const docsLlmsFile of [ + 'packages/docs/public/llms.txt', + 'packages/docs/public/llms-full.txt', + ]) { + expectFile(docsLlmsFile); + if (exists(docsLlmsFile) && fs.lstatSync(abs(docsLlmsFile)).isSymbolicLink()) { + fail(`${docsLlmsFile} must be a real file for docs deployment`); + } + } expectFile('packages/docs/src/generated/version-metadata.json'); if (exists('packages/docs/src/generated/version-metadata.json')) { const docsVersionMetadata = readJson('packages/docs/src/generated/version-metadata.json');