From ba3dac2ca79ca511d8199752eb8bb5855ea5c6d1 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 18 Jan 2026 23:50:22 +0900 Subject: [PATCH 1/6] docs(commands): improve sync skills with native code verification - Add mandatory steps checklist to all sync skills - Add Step 1: Analyze OpenIAP Changes (required) - Update Step 4: Review Native Code with verification commands - Add Decision Matrix for change type vs action required - Add common mistakes to catch section - Add blog post requirement for every sync - Add llms.txt update guidance The key improvement is catching INPUT option fields that require explicit native code updates (e.g., includeSuspendedAndroid). Co-Authored-By: Claude Opus 4.5 --- .claude/commands/sync-all-platforms.md | 67 ++- .claude/commands/sync-expo-iap.md | 613 +++++++++++----------- .claude/commands/sync-flutter-iap.md | 571 +++++++++----------- .claude/commands/sync-godot-iap.md | 507 ++++++++++-------- .claude/commands/sync-kmp-iap.md | 532 +++++++++---------- .claude/commands/sync-react-native-iap.md | 516 +++++++++--------- 6 files changed, 1404 insertions(+), 1402 deletions(-) diff --git a/.claude/commands/sync-all-platforms.md b/.claude/commands/sync-all-platforms.md index 7cd2a5ff..48849d79 100644 --- a/.claude/commands/sync-all-platforms.md +++ b/.claude/commands/sync-all-platforms.md @@ -2,14 +2,27 @@ Master workflow to synchronize OpenIAP changes across all platform SDKs. +## CRITICAL: The #1 Mistake to Avoid + +**The most common sync mistake is updating types without verifying native code passes new options.** + +Example: A new `includeSuspendedAndroid` option is added to `PurchaseOptions`. You: +1. Update types ✓ +2. Run tests ✓ +3. Push PR ✓ + +**BUT**: The native code still passes `null` instead of the options object. The feature doesn't work. + +**ALWAYS verify native code actually passes new input fields to OpenIAP.** + ## Environment Setup Set these environment variables before running sync commands: ```bash # Add to your shell profile (.bashrc, .zshrc, etc.) -export IAP_REPOS_HOME="/Users/hyo/Github/hyochan" # Parent directory of platform SDKs -export OPENIAP_HOME="/Users/hyo/Github/hyodotdev" # Parent directory of openiap monorepo +export IAP_REPOS_HOME="/Users/crossplatformkorea/Github/hyochan" # Parent directory of platform SDKs +export OPENIAP_HOME="/Users/crossplatformkorea/Github/hyodotdev" # Parent directory of openiap monorepo ``` ## Target Repositories @@ -256,7 +269,37 @@ flutter run -d android --- -## Native Code Modification Checklist +## Native Code Modification Checklist (CRITICAL) + +**This is the most important section. DO NOT SKIP.** + +### Change Type Decision Matrix + +| Change Type | Action Required | +|-------------|-----------------| +| New response types only | NO code change - OpenIAP returns them automatically | +| New INPUT option fields | **CHECK** - verify native code passes the option | +| New API function | YES - add wrapper in native + expose to JS/Dart/Kotlin | +| Breaking type change | YES - check serialization compatibility | +| New platform feature | YES - full implementation needed | + +### How to Verify INPUT Options Are Passed + +**For each new input field (e.g., `includeSuspendedAndroid`, `winBackOffer`):** + +1. **Find where the option is used in OpenIAP native SDK:** + ```bash + # Example: Check how OpenIAP uses the option + grep -rn "includeSuspendedAndroid" packages/google/ + ``` + +2. **Check if platform SDK passes the option to OpenIAP:** + ```bash + # Example: Check expo-iap + grep -A10 "getAvailableItems\|getAvailablePurchases" android/src/main/java/expo/modules/iap/ExpoIapModule.kt + ``` + +3. **If native code passes `null` or doesn't read the option, IT WON'T WORK.** ### iOS Native Code Updates @@ -268,12 +311,17 @@ For each platform SDK, check: - [ ] Error handling follows pattern - [ ] Async/await properly used (StoreKit 2) -2. **Type Conversions** +2. **New INPUT Options** + - [ ] Options are read from the params/arguments + - [ ] Options are forwarded to OpenIAP SDK + - [ ] Not passing `null` when options exist + +3. **Type Conversions** - [ ] New types have conversion functions - [ ] Optional fields handled correctly - [ ] Platform-specific fields mapped -3. **Error Handling** +4. **Error Handling** - [ ] New error codes mapped - [ ] Error messages localized if needed - [ ] Proper error propagation @@ -285,12 +333,17 @@ For each platform SDK, check: - [ ] Coroutines/suspend functions properly used - [ ] BillingClient callbacks handled -2. **Type Conversions** +2. **New INPUT Options (MOST COMMONLY MISSED)** + - [ ] Options are parsed from the params Map/HashMap + - [ ] `PurchaseOptions.fromJson(options)` or equivalent is called + - [ ] Options are passed to `openIap.getAvailablePurchases(options)` not `null` + +3. **Type Conversions** - [ ] New types have conversion functions - [ ] Nullable fields handled correctly - [ ] Platform-specific fields mapped -3. **Error Handling** +4. **Error Handling** - [ ] BillingResponseCode mapping updated - [ ] Error messages consistent diff --git a/.claude/commands/sync-expo-iap.md b/.claude/commands/sync-expo-iap.md index 4d42a1d3..f5a02426 100644 --- a/.claude/commands/sync-expo-iap.md +++ b/.claude/commands/sync-expo-iap.md @@ -5,6 +5,25 @@ Synchronize OpenIAP changes to the [expo-iap](https://github.com/hyochan/expo-ia **Target Repository:** `$IAP_REPOS_HOME/expo-iap` > **Note:** Set `IAP_REPOS_HOME` environment variable (see [sync-all-platforms.md](./sync-all-platforms.md#environment-setup)) +> +> **Default Path:** `/Users/crossplatformkorea/Github/hyochan/expo-iap` + +## CRITICAL: Mandatory Steps Checklist + +**YOU MUST COMPLETE ALL THESE STEPS. DO NOT SKIP ANY.** + +| Step | Required | Description | +|------|----------|-------------| +| 0. Pull Latest | **YES** | `git pull` before any work | +| 1. Analyze OpenIAP Changes | **YES** | Review what changed in openiap packages | +| 2. Sync Versions | **YES** | Update openiap-versions.json | +| 3. Generate Types | **YES** | `bun run generate:types` | +| 4. Review Native Code | **YES** | Check if iOS/Android modules need updates | +| 5. Update API Exports | **IF NEEDED** | Add new functions to index.ts, useIAP.ts | +| 6. Run All Checks | **YES** | `bun run lint:ci`, `bun run test`, example tests | +| 7. Write Blog Post | **YES** | Create release notes in `docs/blog/` | +| 8. Update llms.txt | **IF API CHANGED** | Update AI reference docs | +| 9. Commit & Push | **YES** | Create PR with proper format | ## Project Overview @@ -23,450 +42,416 @@ Synchronize OpenIAP changes to the [expo-iap](https://github.com/hyochan/expo-ia | `src/modules/ios.ts` | iOS-specific functions | NO | | `src/modules/android.ts` | Android-specific functions | NO | | `openiap-versions.json` | Version tracking | NO | +| `docs/blog/` | Release blog posts | NO | +| `docs/static/llms.txt` | AI reference (short) | NO | +| `docs/static/llms-full.txt` | AI reference (detailed) | NO | + +--- ## Sync Steps -### 0. Pull Latest (REQUIRED) +### Step 0: Pull Latest (REQUIRED) **Always pull the latest code before starting any sync work:** ```bash -cd $IAP_REPOS_HOME/expo-iap +cd /Users/crossplatformkorea/Github/hyochan/expo-iap git pull ``` -### 1. Sync openiap-versions.json (REQUIRED) +--- + +### Step 1: Analyze OpenIAP Changes (REQUIRED) + +**CRITICAL: Before syncing, understand what changed in the openiap monorepo.** -**IMPORTANT:** Before generating types, sync version numbers from openiap monorepo. +#### 1.1 Check Version Differences ```bash -cd $IAP_REPOS_HOME/expo-iap +echo "=== OpenIAP Monorepo Versions ===" +cat /Users/crossplatformkorea/Github/hyodotdev/openiap/openiap-versions.json + +echo "=== expo-iap Current Versions ===" +cat /Users/crossplatformkorea/Github/hyochan/expo-iap/openiap-versions.json +``` -# Check current versions in openiap monorepo -cat $OPENIAP_HOME/openiap/openiap-versions.json +#### 1.2 Analyze GQL Schema Changes (Types) -# Update expo-iap's openiap-versions.json to match: -# - "gql": should match openiap's "gql" version -# - "apple": should match openiap's "apple" version -# - "google": should match openiap's "google" version +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log --oneline packages/gql/ -10 ``` -**Version fields to sync:** -| Field | Source | Purpose | -|-------|--------|---------| -| `gql` | `$OPENIAP_HOME/openiap/openiap-versions.json` | TypeScript types version | -| `apple` | `$OPENIAP_HOME/openiap/openiap-versions.json` | iOS native SDK version | -| `google` | `$OPENIAP_HOME/openiap/openiap-versions.json` | Android native SDK version | +Look for: +- New types/interfaces added +- New fields on existing types +- Breaking changes to type signatures -### 2. Type Synchronization +#### 1.3 Analyze Apple Package Changes (iOS Native) ```bash -cd $IAP_REPOS_HOME/expo-iap +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log --oneline packages/apple/ -10 +``` -# Download and regenerate types (uses versions from openiap-versions.json) -bun run generate:types +Check `packages/apple/Sources/` for: +- New public functions in `OpenIapModule.swift` +- New types in `Types.swift` +- Changes to serialization in `OpenIapSerialization.swift` -# Verify types -bun run typecheck +#### 1.4 Analyze Google Package Changes (Android Native) + +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log --oneline packages/google/ -10 ``` -### 3. Native Code Modifications +Check `packages/google/openiap/src/main/` for: +- New public functions in `OpenIapModule.kt` +- New types in `Types.kt` +- Changes to Billing Library integration -#### iOS Native Code +#### 1.5 Document Changes Found -**Location:** `ios/` +Create a mental checklist: +- [ ] New types added? +- [ ] New API methods exposed? +- [ ] Breaking changes? +- [ ] Deprecations? +- [ ] Bug fixes? +- [ ] Platform version requirements changed? -Key files to update: -- `ios/ExpoIapModule.swift` - Main Expo module implementation -- `ios/ExpoIap.podspec` - CocoaPods spec (update `apple` version dependency) +--- -**When to modify:** -- New iOS-specific API methods added to OpenIAP -- Type conversion changes needed -- StoreKit 2 API changes +### Step 2: Sync openiap-versions.json (REQUIRED) -**Update workflow:** -```bash -cd $IAP_REPOS_HOME/expo-iap +Update expo-iap's version tracking file to match openiap monorepo. -# 1. Update apple version in openiap-versions.json -# 2. Review openiap/packages/apple/Sources/ for changes -# 3. Update ios/ExpoIapModule.swift accordingly +**Edit `/Users/crossplatformkorea/Github/hyochan/expo-iap/openiap-versions.json`:** -# Install updated pod -cd example/ios && pod install --repo-update +```json +{ + "apple": "", + "google": "", + "gql": "" +} ``` -#### Android Native Code - -**Location:** `android/src/main/java/` - -Key files to update: -- `ExpoIapModule.kt` - Main Expo module implementation -- `build.gradle` - Dependencies (auto-reads `google` version) +--- -**When to modify:** -- New Android-specific API methods added to OpenIAP -- Type conversion changes needed -- Play Billing API changes +### Step 3: Generate Types (REQUIRED) -**Update workflow:** ```bash -cd $IAP_REPOS_HOME/expo-iap +cd /Users/crossplatformkorea/Github/hyochan/expo-iap -# 1. Update google version in openiap-versions.json -# 2. Review openiap/packages/google/openiap/src/main/ for changes -# 3. Update android/src/main/java/ accordingly +# Download and regenerate types +bun run generate:types -# Gradle auto-syncs on build +# Review what changed +git diff src/types.ts ``` -### 4. Build & Test Native Code +**Analyze the type diff carefully:** +- New interfaces/types? +- New fields on existing types? +- Changed field types? +- New enums or enum values? -#### iOS Build Test +--- -```bash -cd $IAP_REPOS_HOME/expo-iap/example +### Step 4: Review Native Code (REQUIRED) -# Clean and prebuild -npx expo prebuild --clean --platform ios +**CRITICAL: This step catches bugs that "type-only" syncs miss. DO NOT SKIP.** -# Install pods -cd ios && pod install --repo-update && cd .. +You must verify that expo-iap's native code actually passes new options/fields to OpenIAP. -# Build for simulator -npx expo run:ios --device "iPhone 15 Pro" +#### 4.1 iOS Native Code Review -# Or build via Xcode -open ios/expoiapexample.xcworkspace -# Build: Cmd+B, Run: Cmd+R -``` +**Location:** `ios/ExpoIapModule.swift`, `ios/ExpoIapHelper.swift` -#### Android Build Test +**Verification steps:** -```bash -cd $IAP_REPOS_HOME/expo-iap/example +1. Check what new fields were added to `RequestPurchaseIosProps` in types: + ```bash + git diff src/types.ts | grep -A5 "RequestPurchaseIosProps\|RequestSubscriptionIosProps" + ``` -# Clean and prebuild -npx expo prebuild --clean --platform android +2. Verify expo-iap passes these fields to OpenIAP: + ```bash + # iOS uses OpenIapSerialization.decode which auto-handles new fields + # BUT: Check if any explicit parameter passing exists that needs updating + grep -n "requestPurchase\|RequestPurchase" ios/ExpoIapModule.swift + ``` -# Build debug APK -npx expo run:android +3. Check if new options fields (like `PurchaseOptions`) are passed: + ```bash + grep -n "getAvailableItems\|getAvailablePurchases" ios/ExpoIapModule.swift + ``` -# Or build via Android Studio -# Open android/ folder in Android Studio -# Build > Make Project -``` +**iOS typically auto-handles new fields via serialization, but verify!** -#### Android Horizon Build (Meta Quest) +#### 4.2 Android Native Code Review -```bash -cd $IAP_REPOS_HOME/expo-iap/example +**Location:** `android/src/main/java/expo/modules/iap/ExpoIapModule.kt` -# Enable Horizon flavor in gradle.properties -echo "horizonEnabled=true" >> android/gradle.properties +**Verification steps:** -# Prebuild and build with Horizon -npx expo prebuild --clean --platform android -npx expo run:android +1. Check what new fields were added to Android types: + ```bash + git diff src/types.ts | grep -A5 "Android" + ``` -# Revert for Play Store builds -sed -i '' '/horizonEnabled=true/d' android/gradle.properties -``` +2. **CRITICAL**: Check if expo-iap's native functions pass options to OpenIAP: + ```bash + # Look for functions that might need options parameter updates + grep -n "openIap\." android/src/main/java/expo/modules/iap/ExpoIapModule.kt | head -20 + ``` -#### Full Build Matrix +3. Verify options are forwarded: + ```bash + # Check if getAvailablePurchases receives and passes options + grep -A10 "getAvailableItems" android/src/main/java/expo/modules/iap/ExpoIapModule.kt + ``` -```bash -cd $IAP_REPOS_HOME/expo-iap - -# TypeScript build -bun run build +**Android often requires explicit option passing - don't assume it works!** -# iOS build -cd example && npx expo run:ios +#### 4.3 TypeScript API Review -# Android build (Play Store) -cd example && npx expo run:android +**Location:** `src/index.ts`, `src/modules/android.ts`, `src/modules/ios.ts` -# Android build (Horizon) -cd example && echo "horizonEnabled=true" >> android/gradle.properties && npx expo run:android +Check if TypeScript API passes new options to native: -# All tests -bun run test -cd example && bun run test +```bash +# Check if new options fields are included in the native call +grep -A20 "getAvailablePurchases" src/index.ts ``` -### 5. Local OpenIAP Testing (Pre-Deployment) +**If a new option exists in types but isn't passed in the TS/native layer, IT WON'T WORK!** -**IMPORTANT:** expo-iap supports testing local openiap changes before deployment. +#### 4.4 Decision Matrix (Updated) -#### Enable Local Development +| Change Type | Action Required | +|-------------|-----------------| +| New types only (response types) | NO code change - OpenIAP returns them automatically | +| New INPUT option fields | **CHECK** - verify native code passes the option | +| New API function | YES - add wrapper in both native + TS | +| Breaking type change | YES - check serialization compatibility | +| New platform feature | YES - add wrapper + expose to JS | -In `example/app.config.ts`: +**Common mistakes to catch:** +- TypeScript has the option in types, but native code passes `null` instead of the options object +- Android native doesn't parse the new option from the params map +- iOS Swift code doesn't include new field in the props struct -```typescript -const LOCAL_OPENIAP_PATHS = { - ios: '/packages/apple', - android: '/packages/google', -} as const; - -export default ({config}: ConfigContext): ExpoConfig => { - // ... - const pluginEntries: NonNullable = [ - [ - '../app.plugin.js', - { - iapkitApiKey: process.env.EXPO_PUBLIC_IAPKIT_API_KEY, - enableLocalDev: true, // <-- Enable local openiap - localPath: { - ios: LOCAL_OPENIAP_PATHS.ios, - android: LOCAL_OPENIAP_PATHS.android, - }, - }, - ], - ]; - // ... -}; -``` +--- -#### Local Dev Workflow +### Step 5: Update API Exports (IF NEEDED) -```bash -# 1. Make changes in openiap monorepo -cd $OPENIAP_HOME/openiap/packages/apple # or packages/google +If new API functions were added to the native modules, expose them in TypeScript: -# 2. Enable local dev in expo-iap -cd $IAP_REPOS_HOME/expo-iap/example -# Edit app.config.ts: set enableLocalDev: true +#### 5.1 Update `src/modules/ios.ts` -# 3. Prebuild with local sources -npx expo prebuild --clean +For iOS-specific functions with `IOS` suffix. -# 4. Build and test -npx expo run:ios # iOS with local openiap-apple -npx expo run:android # Android with local openiap-google +#### 5.2 Update `src/modules/android.ts` -# 5. After testing, disable local dev before committing -# Edit app.config.ts: set enableLocalDev: false -``` +For Android-specific functions with `Android` suffix. -**When to use local dev:** -- Testing new openiap features before release -- Debugging native code issues -- Verifying type generation changes -- Testing breaking changes - -### 6. Update Example Code (REQUIRED) - -**Location:** `example/app/` - -Key example screens: -- `index.tsx` - Home/Overview -- `purchase-flow.tsx` - Purchase flow demo -- `subscription-flow.tsx` - Subscription demo -- `alternative-billing.tsx` - Android alt billing -- `offer-code.tsx` - Promo code redemption - -**Example Code Guidelines:** -- Demonstrate ALL new API features with working code -- Show both success and error handling -- Include comments explaining the feature -- Use realistic SKU names and user flows - -**Example for new iOS feature (e.g., Win-Back Offer):** - -```tsx -// In subscription-flow.tsx -const handleWinBackOffer = async () => { - try { - const result = await requestSubscription({ - sku: 'premium_monthly', - winBackOffer: { offerId: 'winback_50_off' } // iOS 18+ - }); - console.log('Win-back applied:', result); - } catch (error) { - console.error('Win-back failed:', error); - } -}; -``` +#### 5.3 Update `src/index.ts` -**Example for new Android feature (e.g., Product Status):** - -```tsx -// In purchase-flow.tsx -products.forEach((product) => { - if (product.productStatusAndroid) { - switch (product.productStatusAndroid) { - case 'OK': // Show product - break; - case 'NOT_FOUND': // Show error - break; - case 'NO_OFFERS_AVAILABLE': // Show ineligible message - break; - } - } -}); -``` +Export new functions from the main entry point. -### 7. Update Tests +#### 5.4 Update `src/useIAP.ts` -**Library Tests:** `src/__tests__/` -**Example Tests:** `example/__tests__/` +If the new function should be available in the hook. + +--- + +### Step 6: Run All Checks (REQUIRED) + +**ALL checks must pass before proceeding.** ```bash -# Run all tests +cd /Users/crossplatformkorea/Github/hyochan/expo-iap + +# Run full lint suite (tsc, eslint, prettier, ktlint) +bun run lint:ci + +# Run library tests bun run test -# Run example tests -cd example && bun run test +# Run example app tests +cd example && bun run test && cd .. ``` -### 8. Update Documentation (REQUIRED) +**If any check fails, fix before continuing.** -**Location:** `docs/` -- `docs/docs/api/` - API reference -- `docs/docs/types/` - Type definitions -- `docs/docs/guides/` - Usage guides -- `docs/docs/examples/` - Code examples +--- -**Documentation Checklist:** +### Step 7: Write Blog Post (REQUIRED) -For each new feature synced from openiap: +**Every sync MUST have a blog post documenting the changes.** -- [ ] **CHANGELOG.md** - Add entry for new version -- [ ] **API docs** - Function added with signature, params, return type -- [ ] **Type docs** - New types documented with all fields explained -- [ ] **Example code** - Working examples in documentation -- [ ] **Platform notes** - Version requirements (e.g., "iOS 18+", "Billing 8.0+") -- [ ] **Migration notes** - Breaking changes documented +#### 7.1 Create Blog Post File -**Example Documentation Entry:** +**Location:** `docs/blog/` -```mdx -## requestSubscription +**Filename format:** `YYYY-MM-DD--.md` -### Win-Back Offers (iOS 18+) +Example: `2026-01-18-3.5.0-winback-offers.md` -Win-back offers re-engage churned subscribers: +#### 7.2 Blog Post Template -~~~typescript -await requestSubscription({ - sku: 'premium_monthly', - winBackOffer: { offerId: 'winback_50_off' } // iOS 18+ -}); -~~~ -``` +```markdown +--- +slug: - +title: - +authors: [hyochan] +tags: [release, openiap, ] +date: YYYY-MM-DD +--- -### 9. Update llms.txt Files +# Release Notes -**Location:** `docs/static/` +This release syncs with [OpenIAP v](https://www.openiap.dev/docs/updates/notes#). -Update AI-friendly documentation files when APIs or types change: +## New Features -- `docs/static/llms.txt` - Quick reference for AI assistants -- `docs/static/llms-full.txt` - Detailed AI reference +### ( +) -**When to update:** -- New API functions added -- Function signatures changed -- New types or enums added -- Usage patterns updated -- Error codes changed + -**Content to sync:** -1. Installation commands -2. Core API reference (useIAP hook, direct functions) -3. Key types (Product, Purchase, ErrorCode) -4. Common usage patterns -5. Platform-specific APIs (iOS/Android suffixes) -6. Error handling examples +```typescript +// Example usage +``` -### 10. Pre-commit Checklist +## Bug Fixes -```bash -bun run lint # ESLint -bun run typecheck # TypeScript -bun run test # Jest -cd example && bun run test # Example app tests +- + +## OpenIAP Versions + +| Package | Version | +|---------|---------| +| openiap-gql | | +| openiap-google | | +| openiap-apple | | + +For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes#). ``` -**Full Sync Checklist:** +#### 7.3 Blog Post Guidelines + +- **New features**: Explain what they do, show example code, note platform requirements +- **Breaking changes**: MUST have migration guide with before/after code +- **Type-only changes**: Still document, mention "TypeScript types updated" +- **Bug fixes**: List what was fixed +- **Always link**: Link to OpenIAP release notes -- [ ] openiap-versions.json synced -- [ ] Types regenerated (`bun run generate:types`) -- [ ] Native code updated (iOS/Android) -- [ ] Example code demonstrates new features -- [ ] Tests pass -- [ ] Documentation updated -- [ ] llms.txt files updated -- [ ] Local dev disabled (`enableLocalDev: false`) +--- -### 11. Commit and Push +### Step 8: Update llms.txt (IF API CHANGED) -After completing all sync steps, create a branch and commit the changes: +**Location:** `docs/static/llms.txt` and `docs/static/llms-full.txt` + +Update if: +- New API functions added +- Function signatures changed +- New types developers need to know about +- Usage patterns updated + +--- + +### Step 9: Commit and Push (REQUIRED) + +#### 9.1 Create Feature Branch ```bash -cd $IAP_REPOS_HOME/expo-iap +cd /Users/crossplatformkorea/Github/hyochan/expo-iap -# Create feature branch with version number git checkout -b feat/openiap-sync- +# Example: feat/openiap-sync-1.3.13 +``` -# Example: feat/openiap-sync-1.3.12 +#### 9.2 Stage All Changes -# Stage all changes +```bash git add . +``` -# Commit with descriptive message -git commit -m "feat: sync with openiap v +#### 9.3 Commit with Descriptive Message + +```bash +git commit -m "$(cat <<'EOF' +feat: sync with openiap v -- Update openiap-versions.json (gql: , apple: , google: ) +- Update openiap-versions.json (gql: , apple: , google: ) - Regenerate TypeScript types -- Update example code for new types -- Update documentation and llms.txt -- Add/update tests for new features +- +- Add release blog post +- -Co-Authored-By: Claude Opus 4.5 " +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +#### 9.4 Push to Remote -# Push to remote +```bash git push -u origin feat/openiap-sync- ``` -**Branch naming conventions:** -- Feature sync: `feat/openiap-sync-` (e.g., `feat/openiap-sync-1.3.12`) -- Specific feature: `feat/` (e.g., `feat/discount-offer-types`) -- Bug fix: `fix/` (e.g., `fix/subscription-offer-parsing`) +--- -## Naming Conventions +## Version Bump Guidelines -- **iOS-only:** `functionNameIOS` (e.g., `syncIOS`, `getPromotedProductIOS`) -- **Android-only:** `functionNameAndroid` (e.g., `validateReceiptAndroid`) -- **Cross-platform:** No suffix (e.g., `fetchProducts`, `requestPurchase`) -- **Error codes:** kebab-case (e.g., `'user-cancelled'`) +After sync, the expo-iap version should be bumped: -## Deprecation Check +| Change Type | Version Bump | +|-------------|--------------| +| Type-only changes (no API) | PATCH (x.x.+1) | +| New features (non-breaking) | MINOR (x.+1.0) | +| Breaking changes | MAJOR (+1.0.0) | -Search for deprecated patterns: -```bash -cd $IAP_REPOS_HOME/expo-iap -grep -r "@deprecated" src/ -grep -r "DEPRECATED" src/ -``` +**Note:** Version bump is typically done separately after PR merge. + +--- -Known deprecated functions: -- `requestProducts` -> Use `fetchProducts` -- `validateReceipt` -> Use `verifyPurchase` -- `validateReceiptIOS` -> Use `verifyPurchase` +## Example Sync Session -## Commit Message Format +Here's what a complete sync looks like: -```text -feat: add discount offer support -fix: resolve iOS purchase verification -docs: update subscription flow guide ``` +1. git pull +2. Check versions: gql 1.3.12 → 1.3.13, apple 1.3.10 → 1.3.11, google 1.3.23 → 1.3.24 +3. Analyze: Found new WinBackOfferInputIOS type, ProductStatusAndroid type +4. Update openiap-versions.json +5. bun run generate:types +6. Review types diff: +WinBackOfferInputIOS, +ProductStatusAndroid, +PromotionalOfferJwsInputIOS +7. Check native code: Types flow through automatically, no wrapper changes needed +8. bun run lint:ci ✓ +9. bun run test ✓ +10. cd example && bun run test ✓ +11. Create docs/blog/2026-01-18-3.5.0-winback-offers.md +12. git checkout -b feat/openiap-sync-1.3.13 +13. git add . && git commit +14. git push +``` + +--- + +## Naming Conventions + +- **iOS-only:** `functionNameIOS` (e.g., `syncIOS`, `getPromotedProductIOS`) +- **Android-only:** `functionNameAndroid` (e.g., `consumePurchaseAndroid`) +- **Cross-platform:** No suffix (e.g., `fetchProducts`, `requestPurchase`) +- **Error codes:** kebab-case (e.g., `'user-cancelled'`) + +--- ## References -- **CLAUDE.md:** `$IAP_REPOS_HOME/expo-iap/CLAUDE.md` +- **expo-iap CLAUDE.md:** `/Users/crossplatformkorea/Github/hyochan/expo-iap/CLAUDE.md` - **OpenIAP Docs:** [openiap.dev/docs](https://openiap.dev/docs) -- **expo-iap Docs:** [expo-iap.vercel.app](https://expo-iap.vercel.app) +- **expo-iap Docs:** [hyochan.github.io/expo-iap](https://hyochan.github.io/expo-iap) diff --git a/.claude/commands/sync-flutter-iap.md b/.claude/commands/sync-flutter-iap.md index e62396ac..ff2d7f97 100644 --- a/.claude/commands/sync-flutter-iap.md +++ b/.claude/commands/sync-flutter-iap.md @@ -5,6 +5,25 @@ Synchronize OpenIAP changes to the [flutter_inapp_purchase](https://github.com/h **Target Repository:** `$IAP_REPOS_HOME/flutter_inapp_purchase` > **Note:** Set `IAP_REPOS_HOME` environment variable (see [sync-all-platforms.md](./sync-all-platforms.md#environment-setup)) +> +> **Default Path:** `/Users/crossplatformkorea/Github/hyochan/flutter_inapp_purchase` + +## CRITICAL: Mandatory Steps Checklist + +**YOU MUST COMPLETE ALL THESE STEPS. DO NOT SKIP ANY.** + +| Step | Required | Description | +|------|----------|-------------| +| 0. Pull Latest | **YES** | `git pull` before any work | +| 1. Analyze OpenIAP Changes | **YES** | Review what changed in openiap packages | +| 2. Sync Versions | **YES** | Update openiap-versions.json | +| 3. Generate Types | **YES** | `./scripts/generate-type.sh` | +| 4. Review Native Code | **YES** | Check if iOS/Android plugins need updates | +| 5. Update API Exports | **IF NEEDED** | Add new functions to main class | +| 6. Run All Checks | **YES** | `flutter analyze`, `flutter test` | +| 7. Write Blog Post | **YES** | Create release notes in `docs/blog/` | +| 8. Update llms.txt | **IF API CHANGED** | Update AI reference docs | +| 9. Commit & Push | **YES** | Create PR with proper format | ## Project Overview @@ -23,24 +42,18 @@ Synchronize OpenIAP changes to the [flutter_inapp_purchase](https://github.com/h | `lib/helpers.dart` | Type conversion utilities | NO | | `lib/errors.dart` | Error handling & codes | NO | | `lib/builders.dart` | Request builder DSL | NO | -| `lib/enums.dart` | Custom enums | NO | | `ios/Classes/FlutterInappPurchasePlugin.swift` | iOS implementation | NO | | `android/.../FlutterInappPurchasePlugin.kt` | Android implementation | NO | | `openiap-versions.json` | Version tracking | NO | +| `docs/blog/` | Release blog posts | NO | +| `docs/static/llms.txt` | AI reference (short) | NO | +| `docs/static/llms-full.txt` | AI reference (detailed) | NO | -## Version File Structure - -```json -{ - "apple": "1.3.5", - "google": "1.3.14", - "gql": "1.3.5" -} -``` +--- ## Sync Steps -### 0. Pull Latest (REQUIRED) +### Step 0: Pull Latest (REQUIRED) **Always pull the latest code before starting any sync work:** @@ -49,424 +62,350 @@ cd $IAP_REPOS_HOME/flutter_inapp_purchase git pull ``` -### 1. Sync openiap-versions.json (REQUIRED) +--- + +### Step 1: Analyze OpenIAP Changes (REQUIRED) -**IMPORTANT:** Before generating types, sync version numbers from openiap monorepo. +**CRITICAL: Before syncing, understand what changed in the openiap monorepo.** + +#### 1.1 Check Version Differences ```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase +echo "=== OpenIAP Monorepo Versions ===" +cat /Users/crossplatformkorea/Github/hyodotdev/openiap/openiap-versions.json -# Check current versions in openiap monorepo -cat $OPENIAP_HOME/openiap/openiap-versions.json +echo "=== flutter_inapp_purchase Current Versions ===" +cat $IAP_REPOS_HOME/flutter_inapp_purchase/openiap-versions.json +``` + +#### 1.2 Analyze GQL Schema Changes (Types) -# Update flutter_inapp_purchase's openiap-versions.json to match: -# - "gql": should match openiap's "gql" version -# - "apple": should match openiap's "apple" version -# - "google": should match openiap's "google" version +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/gql/ ``` -**Version fields to sync:** -| Field | Source | Purpose | -|-------|--------|---------| -| `gql` | `$OPENIAP_HOME/openiap/openiap-versions.json` | Dart types version | -| `apple` | `$OPENIAP_HOME/openiap/openiap-versions.json` | iOS native SDK version | -| `google` | `$OPENIAP_HOME/openiap/openiap-versions.json` | Android native SDK version | +Look for: +- New types/interfaces added +- New fields on existing types +- Breaking changes to type signatures -### 2. Type Synchronization +#### 1.3 Analyze Apple Package Changes (iOS Native) ```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/apple/ +``` -# Download and regenerate types (uses versions from openiap-versions.json) -./scripts/generate-type.sh +Check `packages/apple/Sources/` for: +- New public functions in `OpenIapModule.swift` +- New types in `Types.swift` +- Changes to serialization -# Verify -flutter analyze +#### 1.4 Analyze Google Package Changes (Android Native) + +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/google/ ``` -**Types Location:** `lib/types.dart` (4,325+ lines, auto-generated) +Check `packages/google/openiap/src/main/` for: +- New public functions in `OpenIapModule.kt` +- New types in `Types.kt` +- Changes to Billing Library integration -### 3. Native Code Modifications +#### 1.5 Document Changes Found -#### iOS Native Code +Create a mental checklist: +- [ ] New types added? +- [ ] New API methods exposed? +- [ ] Breaking changes? +- [ ] Deprecations? +- [ ] Bug fixes? +- [ ] Platform version requirements changed? -**Location:** `ios/Classes/` +--- -Key files to update: +### Step 2: Sync openiap-versions.json (REQUIRED) -- `FlutterInappPurchasePlugin.swift` - Main Flutter plugin implementation -- Method channel handlers for iOS-specific APIs +Update flutter_inapp_purchase's version tracking file to match openiap monorepo. -**When to modify:** +**Edit `openiap-versions.json`:** -- New iOS-specific API methods added to OpenIAP -- StoreKit 2 API changes -- Type conversion between Swift and Dart -- New method channels needed +```json +{ + "apple": "", + "google": "", + "gql": "" +} +``` -**Update workflow:** +--- + +### Step 3: Generate Types (REQUIRED) ```bash cd $IAP_REPOS_HOME/flutter_inapp_purchase -# 1. Update apple version in openiap-versions.json -# 2. Review openiap/packages/apple/Sources/ for changes -# 3. Update ios/Classes/FlutterInappPurchasePlugin.swift -# 4. Update lib/helpers.dart for new type conversions -``` +# Download and regenerate types +./scripts/generate-type.sh -#### Android Native Code +# Verify +flutter analyze +``` -**Location:** `android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/` +**Types Location:** `lib/types.dart` (4,325+ lines, auto-generated) -Key files to update: +**Review what changed:** +```bash +git diff lib/types.dart | head -100 +``` -- `FlutterInappPurchasePlugin.kt` - Main Flutter plugin implementation -- Method channel handlers for Android-specific APIs +**Analyze the type diff carefully:** +- New classes? +- New fields on existing classes? +- Changed field types? +- New enums or enum values? -**When to modify:** +--- -- New Android-specific API methods added to OpenIAP -- Play Billing API changes -- Type conversion between Kotlin and Dart -- New method channels needed +### Step 4: Review Native Code (REQUIRED) -**Update workflow:** +**CRITICAL: This step catches bugs that "type-only" syncs miss. DO NOT SKIP.** -```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase +You must verify that flutter_inapp_purchase's native code actually passes new options/fields to OpenIAP. -# 1. Update google version in openiap-versions.json -# 2. Review openiap/packages/google/openiap/src/main/ for changes -# 3. Update android/.../FlutterInappPurchasePlugin.kt -# 4. Update lib/helpers.dart for new type conversions -``` +#### 4.1 iOS Native Code Review -#### macOS Native Code +**Location:** `ios/Classes/FlutterInappPurchasePlugin.swift` -**Location:** `macos/Classes/` +**Verification steps:** -- Shares implementation pattern with iOS -- Update alongside iOS changes +1. Check what new fields were added to request types: + ```bash + git diff lib/types.dart | grep -A5 "RequestPurchaseIosProps\|RequestSubscriptionIosProps\|PurchaseOptions" + ``` -### 4. Build & Test Native Code +2. Verify flutter plugin passes these fields to OpenIAP: + ```bash + # Check method channel handlers + grep -n "requestPurchase\|getAvailablePurchases" ios/Classes/FlutterInappPurchasePlugin.swift + ``` -#### iOS Build Test +**When to modify:** +- New iOS-specific API methods added to OpenIAP +- StoreKit 2 API changes +- Type conversion between Swift and Dart +- New method channels needed -```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase +#### 4.2 Android Native Code Review -# Build iOS (no code sign for testing) -flutter build ios --no-codesign +**Location:** `android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt` -# Run on simulator -cd example -flutter run -d "iPhone 15 Pro" +**Verification steps:** -# Or build via Xcode -open example/ios/Runner.xcworkspace -# Build: Cmd+B, Run: Cmd+R -``` +1. Check what new fields were added to Android types: + ```bash + git diff lib/types.dart | grep -A5 "Android" + ``` -#### Android Build Test +2. **CRITICAL**: Check if flutter plugin's native functions pass options to OpenIAP: + ```bash + # Look for functions that might need options parameter updates + grep -n "openIap\.\|getAvailablePurchases" android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt | head -20 + ``` -```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase +3. Verify options are forwarded: + ```bash + # Check method channel handlers + grep -A10 "getAvailablePurchases\|when.*call.method" android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt + ``` -# Build APK -flutter build apk --debug +**Android often requires explicit option passing - don't assume it works!** -# Run on emulator -cd example -flutter run -d emulator-5554 +#### 4.3 Dart API Review -# Or build via Android Studio -# Open example/android/ in Android Studio -# Build > Make Project -``` +**Location:** `lib/flutter_inapp_purchase.dart`, `lib/helpers.dart` -#### macOS Build Test +Check if Dart API passes new options to native: ```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase - -# Build macOS -flutter build macos - -# Run -cd example -flutter run -d macos +# Check if new options fields are included in the method channel call +grep -A20 "getAvailablePurchases" lib/flutter_inapp_purchase.dart ``` -#### Android Horizon Build (Meta Quest) +**If a new option exists in types but isn't passed in the Dart/native layer, IT WON'T WORK!** -```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase +#### 4.4 Decision Matrix (Updated) -# Build with Horizon flavor -flutter build apk --flavor horizon +| Change Type | Action Required | +|-------------|-----------------| +| New types only (response types) | NO code change - OpenIAP returns them automatically | +| New INPUT option fields | **CHECK** - verify native code passes the option | +| New API function | YES - add method channel in both native + Dart | +| Breaking type change | YES - check serialization compatibility | +| New platform feature | YES - add method channel + expose to Dart | -# Run example with Horizon -cd example -flutter run --flavor horizon -``` +**Common mistakes to catch:** +- Dart has the option in types, but native code doesn't read it from the method call arguments +- Android native doesn't parse the new option from the HashMap +- Method channel name mismatch -#### Full Build Matrix +--- -```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase +### Step 5: Update API (IF NEEDED) -# All platforms -flutter build ios --no-codesign -flutter build apk # Play Store -flutter build apk --flavor horizon # Horizon Store -flutter build macos +If new API functions were added: -# All tests -flutter test +#### 5.1 Update `lib/flutter_inapp_purchase.dart` -# Example app tests -cd example && flutter test -``` +Add new methods to the main class. -### 5. Update Helper Functions +#### 5.2 Update `lib/helpers.dart` -If types change, update `lib/helpers.dart`: +Add type conversion for new types: - JSON to object conversions - Platform-specific logic - Type transformations -### 6. Update Error Handling +#### 5.3 Update `lib/errors.dart` -If error codes change, update `lib/errors.dart`: +If error codes changed: - Platform error code mappings - Exception classes -### 7. Update Example Code (REQUIRED) - -**Location:** `example/lib/src/screens/` +--- -Key screens: -- `purchase_flow_screen.dart` - Purchase flow demo -- `subscription_flow_screen.dart` - Subscription demo -- `alternative_billing_screen.dart` - Android alt billing -- `offer_code_screen.dart` - Code redemption -- `builder_demo_screen.dart` - DSL demonstration +### Step 6: Run All Checks (REQUIRED) -**Example Code Guidelines:** -- Demonstrate ALL new API features with working code -- Show both success and error handling -- Include comments explaining the feature -- Use realistic SKU names and user flows +**ALL checks must pass before proceeding.** -**Example for new iOS feature (e.g., Win-Back Offer):** -```dart -// In subscription_flow_screen.dart -Future _handleWinBackOffer() async { - try { - final result = await FlutterInappPurchase.instance.requestSubscription( - RequestSubscriptionParams( - sku: 'premium_monthly', - winBackOffer: WinBackOfferInputIOS(offerId: 'winback_50_off'), // iOS 18+ - ), - ); - print('Win-back applied: $result'); - } catch (e) { - print('Win-back failed: $e'); - } -} -``` - -**Example for new Android feature (e.g., Product Status):** -```dart -// In purchase_flow_screen.dart -for (final product in products) { - if (product.productStatusAndroid != null) { - switch (product.productStatusAndroid) { - case ProductStatusAndroid.ok: - // Show product - break; - case ProductStatusAndroid.notFound: - // Show error - break; - case ProductStatusAndroid.noOffersAvailable: - // Show ineligible message - break; - default: - break; - } - } -} -``` +```bash +cd $IAP_REPOS_HOME/flutter_inapp_purchase -### 8. Update Tests +# Format (excludes types.dart) +git ls-files '*.dart' | grep -v '^lib/types.dart$' | xargs dart format --page-width 80 --output=none --set-exit-if-changed -**Unit Tests:** `test/` -**Example Tests:** `example/test/` +# Lint +flutter analyze -```bash -# Run all tests +# Test flutter test -# With coverage (excludes types.dart) -flutter test --coverage +# Or run all checks +./scripts/pre-commit-checks.sh ``` -### 9. Update Documentation (REQUIRED) +**If any check fails, fix before continuing.** -**Location:** `docs/` -- Docusaurus site -- `docs/docs/api/` - API reference -- `docs/docs/types/` - Type definitions -- `docs/docs/guides/` - Usage guides -- `docs/docs/examples/` - Code examples +--- -**Documentation Checklist:** +### Step 7: Write Blog Post (REQUIRED) -For each new feature synced from openiap: +**Every sync MUST have a blog post documenting the changes.** -- [ ] **CHANGELOG.md** - Add entry for new version -- [ ] **API docs** - Function added with signature, params, return type -- [ ] **Type docs** - New types documented with all fields explained -- [ ] **Example code** - Working examples in documentation -- [ ] **Platform notes** - Version requirements (e.g., "iOS 18+", "Billing 8.0+") -- [ ] **Migration notes** - Breaking changes documented +#### 7.1 Create Blog Post File -**Example Documentation Entry:** -```mdx -## requestSubscription +**Location:** `docs/blog/` -### Win-Back Offers (iOS 18+) +**Filename format:** `YYYY-MM-DD--.md` -Win-back offers re-engage churned subscribers: +#### 7.2 Blog Post Template -```dart -await FlutterInappPurchase.instance.requestSubscription( - RequestSubscriptionParams( - sku: 'premium_monthly', - winBackOffer: WinBackOfferInputIOS(offerId: 'winback_50_off'), - ), -); -``` -``` +```markdown +--- +slug: - +title: - +authors: [hyochan] +tags: [release, openiap, ] +date: YYYY-MM-DD +--- -### 10. Update llms.txt Files +# Release Notes -**Location:** `docs/static/` +This release syncs with [OpenIAP v](https://www.openiap.dev/docs/updates/notes#). -Update AI-friendly documentation files when APIs or types change: +## New Features -- `docs/static/llms.txt` - Quick reference for AI assistants -- `docs/static/llms-full.txt` - Detailed AI reference +### ( +) -**When to update:** + -- New API functions added -- Function signatures changed -- New types or enums added -- Usage patterns updated -- Error codes changed +```dart +// Example usage +``` -**Content to sync:** +## Bug Fixes -1. Installation (pubspec.yaml) -2. Core API reference (FlutterInappPurchase class) -3. Key types (Product, Purchase, ErrorCode) -4. Common usage patterns (fetch, purchase, finish) -5. Platform-specific APIs (iOS/Android suffixes) -6. Error handling examples +- -### 11. Pre-commit Checklist +## OpenIAP Versions -```bash -# Format (excludes types.dart) -git ls-files '*.dart' | grep -v '^lib/types.dart$' | xargs dart format --page-width 80 --output=none --set-exit-if-changed +| Package | Version | +|---------|---------| +| openiap-gql | | +| openiap-google | | +| openiap-apple | | -# Lint -flutter analyze +For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes#). +``` -# Test -flutter test +#### 7.3 Blog Post Guidelines -# Final format check -dart format --set-exit-if-changed . +- **New features**: Explain what they do, show example code, note platform requirements +- **Breaking changes**: MUST have migration guide with before/after code +- **Type-only changes**: Still document, mention "Dart types updated" +- **Bug fixes**: List what was fixed +- **Always link**: Link to OpenIAP release notes -# Or run all checks -./scripts/pre-commit-checks.sh -``` +--- -**Full Sync Checklist:** +### Step 8: Update llms.txt (IF API CHANGED) + +**Location:** `docs/static/llms.txt` and `docs/static/llms-full.txt` + +Update if: +- New API functions added +- Function signatures changed +- New types developers need to know about +- Usage patterns updated -- [ ] openiap-versions.json synced -- [ ] Types regenerated (`./scripts/generate-type.sh`) -- [ ] Native code updated (iOS/Android) -- [ ] Helper/error functions updated if needed -- [ ] Example code demonstrates new features -- [ ] Tests pass -- [ ] Documentation updated -- [ ] llms.txt files updated +--- -### 12. Commit and Push +### Step 9: Commit and Push (REQUIRED) -After completing all sync steps, create a branch and commit the changes: +#### 9.1 Create Feature Branch ```bash cd $IAP_REPOS_HOME/flutter_inapp_purchase -# Create feature branch with version number git checkout -b feat/openiap-sync- +``` -# Example: feat/openiap-sync-1.3.12 - -# Stage all changes -git add . +#### 9.2 Commit with Descriptive Message -# Commit with descriptive message -git commit -m "feat: sync with openiap v +```bash +git commit -m "$(cat <<'EOF' +feat: sync with openiap v -- Update openiap-versions.json (gql: , apple: , google: ) +- Update openiap-versions.json (gql: , apple: , google: ) - Regenerate Dart types -- Update example code for new types -- Update documentation and llms.txt -- Add/update tests for new features +- +- Add release blog post +- -Co-Authored-By: Claude Opus 4.5 " - -# Push to remote -git push -u origin feat/openiap-sync- +Co-Authored-By: Claude Opus 4.5 +EOF +)" ``` -**Branch naming conventions:** -- Feature sync: `feat/openiap-sync-` (e.g., `feat/openiap-sync-1.3.12`) -- Specific feature: `feat/` (e.g., `feat/discount-offer-types`) -- Bug fix: `fix/` (e.g., `fix/subscription-offer-parsing`) - -## API Patterns - -### Generic Fetch -```dart -final products = await FlutterInappPurchase.instance.fetchProducts( - productIds: ['product_id'], -); -``` +#### 9.3 Push to Remote -### Request Purchase -```dart -final result = await FlutterInappPurchase.instance.requestPurchase( - RequestPurchaseParams( - sku: 'product_id', - // platform-specific options - ), -); +```bash +git push -u origin feat/openiap-sync- ``` -### Handler Typedefs -```dart -// Use QueryHandlers, MutationHandlers with typed callbacks -QueryHandlers handlers = QueryHandlers( - onProducts: (List products) { ... }, -); -``` +--- ## Naming Conventions @@ -475,25 +414,7 @@ QueryHandlers handlers = QueryHandlers( - **IAP codes:** `Iap` when not final suffix (e.g., `IapPurchase`) - **ID fields:** Always `Id` (e.g., `productId`, `subscriptionGroupIdIOS`) -## Deprecation Check - -```bash -cd $IAP_REPOS_HOME/flutter_inapp_purchase -grep -r "@deprecated" lib/ -grep -r "@Deprecated" lib/ -grep -r "DEPRECATED" lib/ -``` - -Known deprecations: -- `getPurchaseHistories()` -> Use `getAvailablePurchases()` - -## Commit Message Format - -``` -feat: add discount offer support -fix: resolve iOS purchase verification -docs: update subscription flow guide -``` +--- ## References diff --git a/.claude/commands/sync-godot-iap.md b/.claude/commands/sync-godot-iap.md index 095883f0..84402be4 100644 --- a/.claude/commands/sync-godot-iap.md +++ b/.claude/commands/sync-godot-iap.md @@ -5,6 +5,25 @@ Synchronize OpenIAP changes to the [godot-iap](https://github.com/hyochan/godot- **Target Repository:** `$IAP_REPOS_HOME/godot-iap` > **Note:** Set `IAP_REPOS_HOME` environment variable (see [sync-all-platforms.md](./sync-all-platforms.md#environment-setup)) +> +> **Default Path:** `/Users/crossplatformkorea/Github/hyochan/godot-iap` + +## CRITICAL: Mandatory Steps Checklist + +**YOU MUST COMPLETE ALL THESE STEPS. DO NOT SKIP ANY.** + +| Step | Required | Description | +|------|----------|-------------| +| 0. Pull Latest | **YES** | `git pull` before any work | +| 1. Analyze OpenIAP Changes | **YES** | Review what changed in openiap packages | +| 2. Sync Versions | **YES** | Update openiap-versions.json | +| 3. Generate Types | **YES** | Generate in openiap, copy to godot-iap | +| 4. Review Native Code | **YES** | Check if GDExtension code needs updates | +| 5. Update GDScript API | **IF NEEDED** | Add new functions to `iap.gd`, `store.gd` | +| 6. Run All Checks | **YES** | Editor test, GDUnit4 tests | +| 7. Write Blog Post | **YES** | Create release notes | +| 8. Update llms.txt | **IF API CHANGED** | Update AI reference docs | +| 9. Commit & Push | **YES** | Create PR with proper format | ## Project Overview @@ -22,17 +41,21 @@ Synchronize OpenIAP changes to the [godot-iap](https://github.com/hyochan/godot- | `addons/openiap/store.gd` | SwiftUI-like store | NO | | `plugin.cfg` | Plugin configuration | NO | | `openiap-versions.json` | Version tracking | NO | +| `docs/blog/` | Release blog posts | NO | +| `docs/static/llms.txt` | AI reference | NO | ## Type Generation Source **OpenIAP has a built-in GDScript type generator:** -- **Generator:** `$OPENIAP_HOME/openiap/packages/gql/scripts/generate-gdscript-types.mjs` -- **Output:** `$OPENIAP_HOME/openiap/packages/gql/src/generated/types.gd` +- **Generator:** `/Users/crossplatformkorea/Github/hyodotdev/openiap/packages/gql/scripts/generate-gdscript-types.mjs` +- **Output:** `/Users/crossplatformkorea/Github/hyodotdev/openiap/packages/gql/src/generated/types.gd` + +--- ## Sync Steps -### 0. Pull Latest (REQUIRED) +### Step 0: Pull Latest (REQUIRED) **Always pull the latest code before starting any sync work:** @@ -41,33 +64,92 @@ cd $IAP_REPOS_HOME/godot-iap git pull ``` -### 1. Sync openiap-versions.json (REQUIRED) +--- + +### Step 1: Analyze OpenIAP Changes (REQUIRED) + +**CRITICAL: Before syncing, understand what changed in the openiap monorepo.** -**IMPORTANT:** Before generating types, sync version numbers from openiap monorepo. +#### 1.1 Check Version Differences ```bash -cd $IAP_REPOS_HOME/godot-iap +echo "=== OpenIAP Monorepo Versions ===" +cat /Users/crossplatformkorea/Github/hyodotdev/openiap/openiap-versions.json + +echo "=== godot-iap Current Versions ===" +cat $IAP_REPOS_HOME/godot-iap/openiap-versions.json +``` + +#### 1.2 Analyze GQL Schema Changes (Types) + +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/gql/ +``` + +Look for: +- New types/interfaces added +- New fields on existing types +- Breaking changes to type signatures + +#### 1.3 Analyze Apple Package Changes (iOS Native) + +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/apple/ +``` + +Check `packages/apple/Sources/` for: +- New public functions in `OpenIapModule.swift` +- New types in `Types.swift` +- Changes that need GDExtension bridge updates + +#### 1.4 Analyze Google Package Changes (Android Native) -# Check current versions in openiap monorepo -cat $OPENIAP_HOME/openiap/openiap-versions.json +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/google/ +``` + +Check `packages/google/openiap/src/main/` for: +- New public functions in `OpenIapModule.kt` +- New types in `Types.kt` +- Changes that need GDExtension bridge updates + +#### 1.5 Document Changes Found + +Create a mental checklist: +- [ ] New types added? +- [ ] New API methods exposed? +- [ ] Breaking changes? +- [ ] Deprecations? +- [ ] Bug fixes? +- [ ] Platform version requirements changed? + +--- + +### Step 2: Sync openiap-versions.json (REQUIRED) + +Update godot-iap's version tracking file to match openiap monorepo. + +**Edit `openiap-versions.json`:** -# Update godot-iap's openiap-versions.json to match: -# - "gql": should match openiap's "gql" version -# - "apple": should match openiap's "apple" version -# - "google": should match openiap's "google" version +```json +{ + "gql": "", + "apple": "", + "google": "" +} ``` -**Version fields to sync:** -| Field | Source | Purpose | -|-------|--------|---------| -| `gql` | `$OPENIAP_HOME/openiap/openiap-versions.json` | GDScript types version | -| `apple` | `$OPENIAP_HOME/openiap/openiap-versions.json` | iOS native SDK version | -| `google` | `$OPENIAP_HOME/openiap/openiap-versions.json` | Android native SDK version | +--- + +### Step 3: Generate and Copy Types (REQUIRED) -### 2. Generate Types in OpenIAP +#### 3.1 Generate Types in OpenIAP ```bash -cd $OPENIAP_HOME/openiap/packages/gql +cd /Users/crossplatformkorea/Github/hyodotdev/openiap/packages/gql # Run GDScript type generation npm run generate:gdscript @@ -76,68 +158,129 @@ npm run generate:gdscript npm run generate ``` -### 3. Copy Types to godot-iap +#### 3.2 Copy Types to godot-iap ```bash # Copy generated types -cp $OPENIAP_HOME/openiap/packages/gql/src/generated/types.gd \ +cp /Users/crossplatformkorea/Github/hyodotdev/openiap/packages/gql/src/generated/types.gd \ $IAP_REPOS_HOME/godot-iap/addons/openiap/types.gd ``` -### 4. Verify Version Tracking +#### 3.3 Review Changes -Confirm `openiap-versions.json` in godot-iap matches openiap: - -```json -{ - "gql": "1.3.11", - "apple": "1.3.9", - "google": "1.3.21" -} +```bash +cd $IAP_REPOS_HOME/godot-iap +git diff addons/openiap/types.gd | head -100 ``` -### 5. Native Code Modifications +**Analyze the type diff carefully:** +- New classes? +- New fields on existing classes? +- Changed field types? +- New enums or enum values? + +--- + +### Step 4: Review Native Code (REQUIRED) + +**CRITICAL: This step catches bugs that "type-only" syncs miss. DO NOT SKIP.** -#### iOS Native Code (GDExtension) +You must verify that godot-iap's GDExtension code actually passes new options/fields to OpenIAP. + +#### 4.1 iOS GDExtension Review **Location:** `ios/` or `gdextension/ios/` -Key files to update: +**Verification steps:** -- Swift/Objective-C bridge to StoreKit 2 -- GDExtension bindings for iOS +1. Check what new fields were added to iOS types: + ```bash + git diff addons/openiap/types.gd | grep -A5 "IOS\|ios" + ``` -**When to modify:** +2. Check if GDExtension bridge exposes new fields: + ```bash + # Check Swift/Objective-C bridge + grep -rn "getAvailablePurchases\|requestPurchase" ios/ gdextension/ios/ 2>/dev/null + ``` +**When to modify:** - New iOS-specific API methods added to OpenIAP - StoreKit 2 API changes - Type conversion changes -#### Android Native Code (GDExtension) +#### 4.2 Android GDExtension Review **Location:** `android/` or `gdextension/android/` -Key files to update: +**Verification steps:** -- Kotlin/Java bridge to Play Billing -- GDExtension bindings for Android +1. Check what new fields were added to Android types: + ```bash + git diff addons/openiap/types.gd | grep -A5 "Android\|android" + ``` -**When to modify:** +2. **CRITICAL**: Check if GDExtension bridge passes options to OpenIAP: + ```bash + # Check Kotlin/Java bridge + grep -rn "getAvailablePurchases\|requestPurchase" android/ gdextension/android/ 2>/dev/null + ``` -- New Android-specific API methods added to OpenIAP -- Play Billing API changes -- Type conversion changes +**Android often requires explicit option passing - don't assume it works!** + +#### 4.3 GDScript API Review + +**Location:** `addons/openiap/iap.gd`, `addons/openiap/store.gd` + +Check if GDScript API passes new options to native: + +```bash +# Check if new options fields are included in native calls +grep -A10 "get_available_purchases\|request_purchase" addons/openiap/iap.gd +``` + +**If a new option exists in types but isn't passed in the GDScript/native layer, IT WON'T WORK!** -### 6. Update GDScript Implementation +#### 4.4 Decision Matrix (Updated) -If API changes, update: +| Change Type | Action Required | +|-------------|-----------------| +| New types only (response types) | NO code change - OpenIAP returns them automatically | +| New INPUT option fields | **CHECK** - verify GDExtension passes the option | +| New API function | YES - add to GDExtension + GDScript wrapper | +| Breaking type change | YES - check serialization compatibility | +| New platform feature | YES - add GDExtension + GDScript + signals | -- `addons/openiap/iap.gd` - Core module -- `addons/openiap/store.gd` - Store abstraction +**Common mistakes to catch:** +- GDScript has the option in types, but GDExtension doesn't read it +- Android GDExtension doesn't parse the new option +- Signal not emitted for new purchase states -### 7. Build & Test +--- -#### Editor Test +### Step 5: Update GDScript API (IF NEEDED) + +If new API functions were added: + +#### 5.1 Update `addons/openiap/iap.gd` + +Add new methods to the core module. + +#### 5.2 Update `addons/openiap/store.gd` + +Add new methods to the store abstraction. + +#### 5.3 Add Signals + +For new asynchronous events, add signals. + +--- + +### Step 6: Run All Checks (REQUIRED) + +**ALL checks must pass before proceeding.** + +#### 6.1 Editor Test ```bash cd $IAP_REPOS_HOME/godot-iap @@ -149,141 +292,129 @@ godot --editor . # Run test scenes from editor ``` -#### iOS Build Test +#### 6.2 GDUnit4 Tests ```bash -cd $IAP_REPOS_HOME/godot-iap +# Run GDUnit4 tests +godot --headless -s addons/gdunit4/test_runner.gd -# Export iOS build from Godot Editor -# Project > Export > iOS -# Or use CLI: -godot --headless --export-release "iOS" build/ios/godot-iap.ipa +# Or run from editor: GDUnit4 panel > Run Tests ``` -#### Android Build Test +**If any check fails, fix before continuing.** -```bash -cd $IAP_REPOS_HOME/godot-iap +--- -# Export Android build from Godot Editor -# Project > Export > Android -# Or use CLI: -godot --headless --export-release "Android" build/android/godot-iap.apk -``` +### Step 7: Write Blog Post (REQUIRED) -#### Unit Tests (GDUnit4) +**Every sync MUST have a blog post documenting the changes.** -```bash -# Run GDUnit4 tests -godot --headless -s addons/gdunit4/test_runner.gd +#### 7.1 Create Blog Post File -# Or run from editor: GDUnit4 panel > Run Tests -``` +**Location:** `docs/blog/` or `README.md` changelog section + +**Filename format:** `YYYY-MM-DD--.md` -### 8. Update Example Code (REQUIRED) +#### 7.2 Blog Post Template + +```markdown +--- +slug: - +title: - +authors: [hyochan] +tags: [release, openiap, godot, ] +date: YYYY-MM-DD +--- -**Location:** `examples/` +# Release Notes -- Example Godot scenes demonstrating purchase flows -- Sample GDScript code +This release syncs with [OpenIAP v](https://www.openiap.dev/docs/updates/notes#). -**Example Code Guidelines:** -- Demonstrate ALL new API features with working code -- Show both success and error handling -- Include comments explaining the feature -- Use realistic SKU names and user flows +## New Features -**Example for new iOS feature (e.g., Win-Back Offer):** +### ( +) + + ```gdscript -# In example scene script -func _on_winback_button_pressed(): - var request = RequestSubscriptionIosProps.new() - request.sku = "premium_monthly" - request.win_back_offer = WinBackOfferInputIOS.new() - request.win_back_offer.offer_id = "winback_50_off" # iOS 18+ - - var result = await iap.request_subscription_ios(request) - print("Win-back applied: ", result) +# Example usage +var request = RequestSubscriptionIosProps.new() +request.sku = "premium_monthly" +# new option +var result = await iap.request_subscription_ios(request) ``` -**Example for new Android feature (e.g., Product Status):** +## Bug Fixes -```gdscript -# In example scene script -for product in products: - if product.product_status_android != null: - match product.product_status_android: - ProductStatusAndroid.OK: - # Show product - pass - ProductStatusAndroid.NOT_FOUND: - # Show error - pass - ProductStatusAndroid.NO_OFFERS_AVAILABLE: - # Show ineligible message - pass +- + +## OpenIAP Versions + +| Package | Version | +|---------|---------| +| openiap-gql | | +| openiap-google | | +| openiap-apple | | + +For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes#). ``` -### 9. Update Documentation (REQUIRED) +#### 7.3 Blog Post Guidelines -**Location:** `docs/` or `README.md` +- **New features**: Explain what they do, show example code, note platform requirements +- **Breaking changes**: MUST have migration guide with before/after code +- **Type-only changes**: Still document, mention "GDScript types updated" +- **Bug fixes**: List what was fixed +- **Always link**: Link to OpenIAP release notes -**Documentation Checklist:** +--- -For each new feature synced from openiap: +### Step 8: Update llms.txt (IF API CHANGED) -- [ ] **CHANGELOG.md** - Add entry for new version -- [ ] **API reference** - Function added with signature, params, return type -- [ ] **Type reference** - New types documented with all fields explained -- [ ] **Example code** - Working examples in documentation -- [ ] **Platform notes** - Version requirements (e.g., "iOS 18+", "Billing 8.0+") -- [ ] **Migration notes** - Breaking changes documented +**Location:** `docs/static/llms.txt` and `docs/static/llms-full.txt` -**Example Documentation Entry:** +Update if: +- New API functions added +- Function signatures changed +- New types developers need to know about +- Signal patterns updated -```markdown -## request_subscription_ios +--- -### Win-Back Offers (iOS 18+) +### Step 9: Commit and Push (REQUIRED) -Win-back offers re-engage churned subscribers: +#### 9.1 Create Feature Branch -~~~gdscript -var request = RequestSubscriptionIosProps.new() -request.sku = "premium_monthly" -request.win_back_offer = WinBackOfferInputIOS.new() -request.win_back_offer.offer_id = "winback_50_off" +```bash +cd $IAP_REPOS_HOME/godot-iap -var result = await iap.request_subscription_ios(request) -~~~ +git checkout -b feat/openiap-sync- ``` -### 10. Update llms.txt Files +#### 9.2 Commit with Descriptive Message -**Location:** `docs/static/` - -Update AI-friendly documentation files when APIs or types change: +```bash +git commit -m "$(cat <<'EOF' +feat: sync with openiap v -- `docs/static/llms.txt` - Quick reference for AI assistants -- `docs/static/llms-full.txt` - Detailed AI reference +- Update openiap-versions.json (gql: , apple: , google: ) +- Regenerate GDScript types +- +- Add release blog post +- -**When to update:** +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` -- New API functions added -- Function signatures changed -- New types or enums added -- GDScript patterns updated -- Error codes changed +#### 9.3 Push to Remote -**Content to sync:** +```bash +git push -u origin feat/openiap-sync- +``` -1. Installation (Godot Asset Library) -2. Core API reference (IAP module, Store class) -3. Key types (ProductIOS, PurchaseAndroid, etc.) -4. Signal patterns for purchase events -5. Platform-specific notes (StoreKit 2, Play Billing) -6. Error handling examples +--- ## Generated Type Structure @@ -310,6 +441,8 @@ class ProductIOS: pass ``` +--- + ## Naming Conventions (GDScript) - **Classes:** PascalCase (e.g., `ProductIOS`, `RequestPurchaseResult`) @@ -319,88 +452,10 @@ class ProductIOS: - **iOS types:** `IOS` suffix - **Android types:** `Android` suffix -## Deprecation Check - -```bash -cd $IAP_REPOS_HOME/godot-iap -grep -r "deprecated" addons/ -grep -r "DEPRECATED" addons/ -``` - -## Platform-Specific Notes - -**iOS:** -- Uses StoreKit 2 via Swift bridge -- Check Apple-specific types in generated code - -**Android:** -- Uses Google Play Billing via Java/Kotlin bridge -- Check Android-specific types in generated code - -## Pre-commit Checklist - -1. Types generated and copied -2. Implementation updated for new types -3. Examples updated -4. Tests passing -5. Documentation updated - -**Full Sync Checklist:** - -- [ ] openiap-versions.json synced -- [ ] Types regenerated and copied (`types.gd`) -- [ ] GDScript implementation updated (`iap.gd`, `store.gd`) -- [ ] GDExtension native code updated if needed -- [ ] Example code demonstrates new features -- [ ] Tests pass (GDUnit4) -- [ ] Documentation updated -- [ ] llms.txt files updated - -### 11. Commit and Push - -After completing all sync steps, create a branch and commit the changes: - -```bash -cd $IAP_REPOS_HOME/godot-iap - -# Create feature branch with version number -git checkout -b feat/openiap-sync- - -# Example: feat/openiap-sync-1.3.12 - -# Stage all changes -git add . - -# Commit with descriptive message -git commit -m "feat: sync with openiap v - -- Update openiap-versions.json (gql: , apple: , google: ) -- Regenerate GDScript types -- Update example code for new types -- Update documentation and llms.txt -- Add/update tests for new features - -Co-Authored-By: Claude Opus 4.5 " - -# Push to remote -git push -u origin feat/openiap-sync- -``` - -**Branch naming conventions:** -- Feature sync: `feat/openiap-sync-` (e.g., `feat/openiap-sync-1.3.12`) -- Specific feature: `feat/` (e.g., `feat/discount-offer-types`) -- Bug fix: `fix/` (e.g., `fix/subscription-offer-parsing`) - -## Commit Message Format - -``` -feat: add discount offer support -fix: resolve iOS purchase verification -docs: update subscription flow guide -``` +--- ## References -- **OpenIAP GDScript Generator:** `$OPENIAP_HOME/openiap/packages/gql/scripts/generate-gdscript-types.mjs` +- **OpenIAP GDScript Generator:** `/Users/crossplatformkorea/Github/hyodotdev/openiap/packages/gql/scripts/generate-gdscript-types.mjs` - **OpenIAP Docs:** https://openiap.dev/docs - **Godot IAP Docs:** Check README.md diff --git a/.claude/commands/sync-kmp-iap.md b/.claude/commands/sync-kmp-iap.md index d47500e7..78914955 100644 --- a/.claude/commands/sync-kmp-iap.md +++ b/.claude/commands/sync-kmp-iap.md @@ -5,6 +5,25 @@ Synchronize OpenIAP changes to the [kmp-iap](https://github.com/hyochan/kmp-iap) **Target Repository:** `$IAP_REPOS_HOME/kmp-iap` > **Note:** Set `IAP_REPOS_HOME` environment variable (see [sync-all-platforms.md](./sync-all-platforms.md#environment-setup)) +> +> **Default Path:** `/Users/crossplatformkorea/Github/hyochan/kmp-iap` + +## CRITICAL: Mandatory Steps Checklist + +**YOU MUST COMPLETE ALL THESE STEPS. DO NOT SKIP ANY.** + +| Step | Required | Description | +|------|----------|-------------| +| 0. Pull Latest | **YES** | `git pull` before any work | +| 1. Analyze OpenIAP Changes | **YES** | Review what changed in openiap packages | +| 2. Sync Versions | **YES** | Update openiap-versions.json | +| 3. Generate Types | **YES** | `./scripts/generate-types.sh` | +| 4. Review Native Code | **YES** | Check if iOS/Android implementations need updates | +| 5. Update API Exports | **IF NEEDED** | Add new functions, type aliases, DSL builders | +| 6. Run All Checks | **YES** | `./gradlew build test detekt` | +| 7. Write Blog Post | **YES** | Create release notes in `docs/blog/` | +| 8. Update llms.txt | **IF API CHANGED** | Update AI reference docs | +| 9. Commit & Push | **YES** | Create PR with proper format | ## Project Overview @@ -24,21 +43,14 @@ Synchronize OpenIAP changes to the [kmp-iap](https://github.com/hyochan/kmp-iap) | `library/src/commonMain/.../dsl/PurchaseDsl.kt` | DSL builders | NO | | `library/src/commonMain/.../utils/ErrorMapping.kt` | Error code mapping | NO | | `openiap-versions.json` | Version tracking | NO | +| `docs/blog/` | Release blog posts | NO | +| `docs/static/llms.txt` | AI reference (short) | NO | -## Version File Structure - -```json -{ - "gql": "1.2.5", // GraphQL schema version (for Types.kt) - "google": "1.3.7", // Android openiap-google version - "apple": "1.2.39", // iOS openiap pod version - "kmp-iap": "1.0.0-rc.6" // This library version -} -``` +--- ## Sync Steps -### 0. Pull Latest (REQUIRED) +### Step 0: Pull Latest (REQUIRED) **Always pull the latest code before starting any sync work:** @@ -47,350 +59,364 @@ cd $IAP_REPOS_HOME/kmp-iap git pull ``` -### 1. Sync openiap-versions.json (REQUIRED) +--- + +### Step 1: Analyze OpenIAP Changes (REQUIRED) -**IMPORTANT:** Before generating types, sync version numbers from openiap monorepo. +**CRITICAL: Before syncing, understand what changed in the openiap monorepo.** + +#### 1.1 Check Version Differences ```bash -cd $IAP_REPOS_HOME/kmp-iap +echo "=== OpenIAP Monorepo Versions ===" +cat /Users/crossplatformkorea/Github/hyodotdev/openiap/openiap-versions.json -# Check current versions in openiap monorepo -cat $OPENIAP_HOME/openiap/openiap-versions.json +echo "=== kmp-iap Current Versions ===" +cat $IAP_REPOS_HOME/kmp-iap/openiap-versions.json +``` + +#### 1.2 Analyze GQL Schema Changes (Types) -# Update kmp-iap's openiap-versions.json to match: -# - "gql": should match openiap's "gql" version -# - "apple": should match openiap's "apple" version -# - "google": should match openiap's "google" version +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/gql/ ``` -**Version fields to sync:** -| Field | Source | Purpose | -|-------|--------|---------| -| `gql` | `$OPENIAP_HOME/openiap/openiap-versions.json` | Kotlin types version | -| `apple` | `$OPENIAP_HOME/openiap/openiap-versions.json` | iOS native SDK version | -| `google` | `$OPENIAP_HOME/openiap/openiap-versions.json` | Android native SDK version | +Look for: +- New types/interfaces added +- New fields on existing types +- Breaking changes to type signatures -### 2. Type Synchronization +#### 1.3 Analyze Apple Package Changes (iOS Native) ```bash -cd $IAP_REPOS_HOME/kmp-iap +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/apple/ +``` -# Download and regenerate types (uses versions from openiap-versions.json) -./scripts/generate-types.sh +Check `packages/apple/Sources/` for: +- New public functions in `OpenIapModule.swift` +- New types in `Types.swift` +- Changes to serialization -# Verify build -./gradlew :library:compileDebugKotlin +#### 1.4 Analyze Google Package Changes (Android Native) + +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/google/ ``` -**Types Location:** `library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt` +Check `packages/google/openiap/src/main/` for: +- New public functions in `OpenIapModule.kt` +- New types in `Types.kt` +- Changes to Billing Library integration + +#### 1.5 Document Changes Found -### 3. Native Code Modifications +Create a mental checklist: +- [ ] New types added? +- [ ] New API methods exposed? +- [ ] Breaking changes? +- [ ] Deprecations? +- [ ] Bug fixes? +- [ ] Platform version requirements changed? -#### Android Implementation +--- -**Location:** `library/src/androidMain/kotlin/io/github/hyochan/kmpiap/` +### Step 2: Sync openiap-versions.json (REQUIRED) -Key files to update: +Update kmp-iap's version tracking file to match openiap monorepo. -- `InAppPurchaseAndroid.kt` (879 lines) - Main Android implementation -- `Helper.kt` (312 lines) - Android utility functions -- `Platform.kt` - Platform detection -- `KmpIAP.android.kt` - Android entry point +**Edit `openiap-versions.json`:** -**When to modify:** +```json +{ + "gql": "", + "google": "", + "apple": "", + "kmp-iap": "" +} +``` -- New Android-specific API methods added to OpenIAP -- Play Billing API changes -- Type mapping changes +--- -**Update workflow:** +### Step 3: Generate Types (REQUIRED) ```bash cd $IAP_REPOS_HOME/kmp-iap -# 1. Update google version in openiap-versions.json -# 2. Review openiap/packages/google/openiap/src/main/ for changes -# 3. Update library/src/androidMain/.../InAppPurchaseAndroid.kt -# 4. Update Helper.kt for new type conversions +# Download and regenerate types +./scripts/generate-types.sh + +# Verify build +./gradlew :library:compileDebugKotlin ``` -#### iOS Implementation +**Types Location:** `library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt` -**Location:** `library/src/iosMain/kotlin/io/github/hyochan/kmpiap/` +**Review what changed:** +```bash +git diff library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt | head -100 +``` -Key files to update: +**Analyze the type diff carefully:** +- New data classes? +- New fields on existing classes? +- Changed field types? +- New enums or enum values? -- `InAppPurchaseIOS.kt` (880 lines) - Main iOS implementation -- `Platform.kt` - Platform detection -- `KmpIAP.ios.kt` - iOS entry point -- `nativeInterop/cinterop/` - C-Interop declarations for Swift +--- -**When to modify:** +### Step 4: Review Native Code (REQUIRED) -- New iOS-specific API methods added to OpenIAP -- StoreKit 2 API changes -- Swift interop changes +**CRITICAL: This step catches bugs that "type-only" syncs miss. DO NOT SKIP.** -**Update workflow:** +You must verify that kmp-iap's implementations actually pass new options/fields to OpenIAP. -```bash -cd $IAP_REPOS_HOME/kmp-iap +#### 4.1 Android Implementation Review -# 1. Update apple version in openiap-versions.json -# 2. Review openiap/packages/apple/Sources/ for changes -# 3. Update library/src/iosMain/.../InAppPurchaseIOS.kt -# 4. Update cinterop if Swift API signature changed -``` +**Location:** `library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt` -### 4. Build & Test Native Code +**Verification steps:** -#### Android Build Test +1. Check what new fields were added to request types: + ```bash + git diff library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt | grep -A5 "RequestPurchase\|PurchaseOptions" + ``` -```bash -cd $IAP_REPOS_HOME/kmp-iap +2. **CRITICAL**: Check if kmp-iap's Android implementation passes options to OpenIAP: + ```bash + # Look for functions that might need options parameter updates + grep -n "openIap\.\|getAvailablePurchases" library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt | head -20 + ``` -# Compile Android library -./gradlew :library:compileDebugKotlin -./gradlew :library:compileReleaseKotlin +3. Verify options are forwarded: + ```bash + # Check implementation methods + grep -A10 "getAvailablePurchases\|suspend fun" library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt | head -50 + ``` -# Build example app -./gradlew :example:composeApp:assembleDebug +**Android often requires explicit option passing - don't assume it works!** -# Run Android tests -./gradlew :library:testDebugUnitTest +#### 4.2 iOS Implementation Review -# Run on emulator (from Android Studio) -# Open in Android Studio, run example/composeApp -``` +**Location:** `library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt` -#### iOS Build Test +**Verification steps:** -```bash -cd $IAP_REPOS_HOME/kmp-iap +1. Check what new fields were added to iOS types: + ```bash + git diff library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt | grep -A5 "IOS" + ``` -# Build iOS framework -./gradlew :library:linkDebugFrameworkIosSimulatorArm64 -./gradlew :library:linkReleaseFrameworkIosArm64 +2. Verify kmp-iap passes these fields to OpenIAP: + ```bash + # Check Swift interop calls + grep -n "openIap\.\|requestPurchase\|getAvailable" library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt | head -20 + ``` -# Build example iOS app -cd example/iosApp -pod install --repo-update +3. Check cinterop if Swift API signature changed: + ```bash + # Review cinterop declarations + cat library/src/nativeInterop/cinterop/*.def + ``` -# Build via Xcode -xcodebuild -workspace iosApp.xcworkspace \ - -scheme iosApp \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ - build +#### 4.3 Common API Review -# Or open in Xcode and build -open iosApp.xcworkspace -``` +**Location:** `library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt` -#### Full Build Matrix +Check if common interface exposes new options: ```bash -cd $IAP_REPOS_HOME/kmp-iap +# Check if new options fields are included in the interface +grep -A10 "getAvailablePurchases\|suspend fun" library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt +``` -# All tests -./gradlew :library:test +**If a new option exists in types but isn't passed in the implementation, IT WON'T WORK!** -# Full library build -./gradlew :library:build +#### 4.4 Decision Matrix (Updated) -# Code quality -./gradlew :library:detekt +| Change Type | Action Required | +|-------------|-----------------| +| New types only (response types) | NO code change - OpenIAP returns them automatically | +| New INPUT option fields | **CHECK** - verify implementations pass the option | +| New API function | YES - add to interface + both platform implementations | +| Breaking type change | YES - check serialization compatibility | +| New platform feature | YES - add to interface + platform impl + DSL | -# Publish locally for testing -./gradlew publishToMavenLocal -``` +**Common mistakes to catch:** +- Interface has the option but Android/iOS implementation passes `null` +- Android implementation doesn't read the new option from params +- iOS cinterop doesn't map the new Swift parameter + +--- + +### Step 5: Update API (IF NEEDED) + +If new API functions or options were added: + +#### 5.1 Update Type Aliases -### 5. Update Type Aliases +**Location:** `library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt` -If new types added, update `KmpIap.kt`: ```kotlin typealias NewType = io.github.hyochan.kmpiap.openiap.NewType ``` -### 6. Update DSL Builders +#### 5.2 Update DSL Builders + +**Location:** `library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt` -If new request types added, update `dsl/PurchaseDsl.kt`: ```kotlin class NewRequestBuilder { // ... } ``` -### 7. Update Example Code (REQUIRED) +#### 5.3 Update Error Mapping -**Location:** `example/composeApp/` -- Compose Multiplatform shared UI -- iOS app: `example/iosApp/` +**Location:** `library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt` -**Example Code Guidelines:** -- Demonstrate ALL new API features with working code -- Show both success and error handling -- Include comments explaining the feature -- Use realistic SKU names and user flows +If error codes changed. -**Example for new iOS feature (e.g., Win-Back Offer):** -```kotlin -// In Example app Compose UI -Button(onClick = { - scope.launch { - val result = kmpIapInstance.requestSubscription { - ios { - sku = "premium_monthly" - winBackOffer = WinBackOfferInputIOS(offerId = "winback_50_off") // iOS 18+ - } - } - println("Win-back applied: $result") - } -}) { - Text("Apply Win-Back Offer") -} -``` +--- -**Example for new Android feature (e.g., Product Status):** -```kotlin -// In Example app Compose UI -products.forEach { product -> - product.productStatusAndroid?.let { status -> - when (status) { - ProductStatusAndroid.Ok -> { /* Show product */ } - ProductStatusAndroid.NotFound -> { /* Show error */ } - ProductStatusAndroid.NoOffersAvailable -> { /* Show ineligible message */ } - else -> { /* Handle unknown */ } - } - } -} -``` +### Step 6: Run All Checks (REQUIRED) -### 8. Update Tests - -**Location:** `library/src/commonTest/` +**ALL checks must pass before proceeding.** ```bash +cd $IAP_REPOS_HOME/kmp-iap + +# All tests ./gradlew :library:test + +# Full library build ./gradlew :library:build + +# Code quality +./gradlew :library:detekt + +# Or all at once +./gradlew :library:test :library:build :library:detekt ``` -### 9. Update Documentation (REQUIRED) +**If any check fails, fix before continuing.** + +--- + +### Step 7: Write Blog Post (REQUIRED) + +**Every sync MUST have a blog post documenting the changes.** -**Location:** `docs/` -- `docs/docs/api/` - API documentation -- `docs/docs/types/` - Type definitions -- `docs/docs/examples/` - Code examples -- `docs/docs/guides/` - Usage guides +#### 7.1 Create Blog Post File -**Documentation Checklist:** +**Location:** `docs/blog/` -For each new feature synced from openiap: +**Filename format:** `YYYY-MM-DD--.md` -- [ ] **CHANGELOG.md** - Add entry for new version -- [ ] **API docs** - Function added with signature, params, return type -- [ ] **Type docs** - New types documented with all fields explained -- [ ] **Example code** - Working examples in documentation -- [ ] **Platform notes** - Version requirements (e.g., "iOS 18+", "Billing 8.0+") -- [ ] **Migration notes** - Breaking changes documented +#### 7.2 Blog Post Template -**Example Documentation Entry:** -```mdx -## requestSubscription +```markdown +--- +slug: - +title: - +authors: [hyochan] +tags: [release, openiap, kmp, ] +date: YYYY-MM-DD +--- -### Win-Back Offers (iOS 18+) +# Release Notes -Win-back offers re-engage churned subscribers: +This release syncs with [OpenIAP v](https://www.openiap.dev/docs/updates/notes#). + +## New Features + +### ( +) + + ```kotlin +// Example usage kmpIapInstance.requestSubscription { ios { sku = "premium_monthly" - winBackOffer = WinBackOfferInputIOS(offerId = "winback_50_off") + // new option } } ``` -``` -### 10. Update llms.txt Files +## Bug Fixes -**Location:** `docs/static/` +- -Update AI-friendly documentation files when APIs or types change: +## OpenIAP Versions -- `docs/static/llms.txt` - Quick reference for AI assistants -- `docs/static/llms-full.txt` - Detailed AI reference +| Package | Version | +|---------|---------| +| openiap-gql | | +| openiap-google | | +| openiap-apple | | -**When to update:** +For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes#). +``` -- New API functions added -- Function signatures changed -- New types or enums added -- DSL builders updated -- Error codes changed +#### 7.3 Blog Post Guidelines -**Content to sync:** +- **New features**: Explain what they do, show example code, note platform requirements +- **Breaking changes**: MUST have migration guide with before/after code +- **Type-only changes**: Still document, mention "Kotlin types updated" +- **Bug fixes**: List what was fixed +- **Always link**: Link to OpenIAP release notes -1. Installation (Gradle dependencies) -2. Core API reference (KmpIAP interface, DSL patterns) -3. Key types (Product, Purchase, ErrorCode) -4. Platform-specific patterns (ios/android DSL blocks) -5. Error handling examples +--- -### 11. Pre-commit Checklist +### Step 8: Update llms.txt (IF API CHANGED) -```bash -./gradlew :library:test -./gradlew :library:build -./gradlew :library:detekt -``` +**Location:** `docs/static/llms.txt` and `docs/static/llms-full.txt` -**Full Sync Checklist:** +Update if: +- New API functions added +- Function signatures changed +- New types developers need to know about +- DSL builders updated -- [ ] openiap-versions.json synced -- [ ] Types regenerated (`./scripts/generate-types.sh`) -- [ ] Type aliases updated in `KmpIap.kt` -- [ ] DSL builders updated if new request types -- [ ] Native implementations updated (iOS/Android) -- [ ] Example code demonstrates new features -- [ ] Tests pass -- [ ] Documentation updated -- [ ] llms.txt files updated +--- -### 12. Commit and Push +### Step 9: Commit and Push (REQUIRED) -After completing all sync steps, create a branch and commit the changes: +#### 9.1 Create Feature Branch ```bash cd $IAP_REPOS_HOME/kmp-iap -# Create feature branch with version number git checkout -b feat/openiap-sync- +``` -# Example: feat/openiap-sync-1.3.12 - -# Stage all changes -git add . +#### 9.2 Commit with Descriptive Message -# Commit with descriptive message -git commit -m "feat: sync with openiap v +```bash +git commit -m "$(cat <<'EOF' +feat: sync with openiap v -- Update openiap-versions.json (gql: , apple: , google: ) +- Update openiap-versions.json (gql: , apple: , google: ) - Regenerate Kotlin types -- Update example code for new types -- Update documentation and llms.txt -- Add/update tests for new features +- +- Add release blog post +- -Co-Authored-By: Claude Opus 4.5 " +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +#### 9.3 Push to Remote -# Push to remote +```bash git push -u origin feat/openiap-sync- ``` -**Branch naming conventions:** -- Feature sync: `feat/openiap-sync-` (e.g., `feat/openiap-sync-1.3.12`) -- Specific feature: `feat/` (e.g., `feat/discount-offer-types`) -- Bug fix: `fix/` (e.g., `fix/subscription-offer-parsing`) +--- ## API Patterns @@ -402,12 +428,6 @@ val products = kmpIapInstance.fetchProducts { } ``` -### Constructor Pattern (for testing) -```kotlin -val kmpIAP = KmpIAP() -kmpIAP.initConnection() -``` - ### Platform-Specific DSL ```kotlin val purchase = kmpIapInstance.requestPurchase { @@ -416,6 +436,8 @@ val purchase = kmpIapInstance.requestPurchase { } ``` +--- + ## Naming Conventions - **IAP suffix:** For types contrasting with iOS (e.g., `IapPurchase`) @@ -423,39 +445,7 @@ val purchase = kmpIapInstance.requestPurchase { - **iOS types:** `IOS` suffix when iOS-specific - **Android types:** `Android` suffix when Android-specific -## Deprecation Check - -```bash -cd $IAP_REPOS_HOME/kmp-iap -grep -r "@Deprecated" library/src/ -grep -r "DEPRECATED" library/src/ -``` - -## Build Commands - -```bash -# Type generation -./scripts/generate-types.sh - -# Building -./gradlew :library:build -./gradlew :library:compileDebugKotlin -./gradlew :example:composeApp:assembleDebug - -# Testing -./gradlew :library:test - -# Publishing -./gradlew publishAllPublicationsToMavenCentral -``` - -## Commit Message Format - -``` -feat: add discount offer support -fix: resolve iOS purchase verification -docs: update subscription flow guide -``` +--- ## References diff --git a/.claude/commands/sync-react-native-iap.md b/.claude/commands/sync-react-native-iap.md index c74a1a3d..17d5758d 100644 --- a/.claude/commands/sync-react-native-iap.md +++ b/.claude/commands/sync-react-native-iap.md @@ -5,6 +5,25 @@ Synchronize OpenIAP changes to the [react-native-iap](https://github.com/hyochan **Target Repository:** `$IAP_REPOS_HOME/react-native-iap` > **Note:** Set `IAP_REPOS_HOME` environment variable (see [sync-all-platforms.md](./sync-all-platforms.md#environment-setup)) +> +> **Default Path:** `/Users/crossplatformkorea/Github/hyochan/react-native-iap` + +## CRITICAL: Mandatory Steps Checklist + +**YOU MUST COMPLETE ALL THESE STEPS. DO NOT SKIP ANY.** + +| Step | Required | Description | +|------|----------|-------------| +| 0. Pull Latest | **YES** | `git pull` before any work | +| 1. Analyze OpenIAP Changes | **YES** | Review what changed in openiap packages | +| 2. Sync Versions | **YES** | Update openiap-versions.json | +| 3. Generate Types | **YES** | `yarn generate:types` | +| 4. Review Native Code | **YES** | Check if iOS/Android modules need updates | +| 5. Update API Exports | **IF NEEDED** | Add new functions to index.ts | +| 6. Run All Checks | **YES** | `yarn typecheck`, `yarn test` | +| 7. Write Blog Post | **YES** | Create release notes in `docs/blog/` | +| 8. Update llms.txt | **IF API CHANGED** | Update AI reference docs | +| 9. Commit & Push | **YES** | Create PR with proper format | ## Project Overview @@ -26,10 +45,15 @@ Synchronize OpenIAP changes to the [react-native-iap](https://github.com/hyochan | `android/.../HybridRnIap.kt` | Android implementation | NO | | `nitrogen/generated/` | Nitro bridge code | YES | | `openiap-versions.json` | Version tracking | NO | +| `docs/blog/` | Release blog posts | NO | +| `docs/static/llms.txt` | AI reference (short) | NO | +| `docs/static/llms-full.txt` | AI reference (detailed) | NO | + +--- ## Sync Steps -### 0. Pull Latest (REQUIRED) +### Step 0: Pull Latest (REQUIRED) **Always pull the latest code before starting any sync work:** @@ -38,101 +62,193 @@ cd $IAP_REPOS_HOME/react-native-iap git pull ``` -### 1. Sync openiap-versions.json (REQUIRED) +--- + +### Step 1: Analyze OpenIAP Changes (REQUIRED) + +**CRITICAL: Before syncing, understand what changed in the openiap monorepo.** -**IMPORTANT:** Before generating types, sync version numbers from openiap monorepo. +#### 1.1 Check Version Differences ```bash -cd $IAP_REPOS_HOME/react-native-iap +echo "=== OpenIAP Monorepo Versions ===" +cat /Users/crossplatformkorea/Github/hyodotdev/openiap/openiap-versions.json + +echo "=== react-native-iap Current Versions ===" +cat $IAP_REPOS_HOME/react-native-iap/openiap-versions.json +``` -# Check current versions in openiap monorepo -cat $OPENIAP_HOME/openiap/openiap-versions.json +#### 1.2 Analyze GQL Schema Changes (Types) -# Update react-native-iap's openiap-versions.json to match: -# - "gql": should match openiap's "gql" version -# - "apple": should match openiap's "apple" version -# - "google": should match openiap's "google" version +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/gql/ ``` -**Version fields to sync:** -| Field | Source | Purpose | -|-------|--------|---------| -| `gql` | `$OPENIAP_HOME/openiap/openiap-versions.json` | TypeScript types version | -| `apple` | `$OPENIAP_HOME/openiap/openiap-versions.json` | iOS native SDK version | -| `google` | `$OPENIAP_HOME/openiap/openiap-versions.json` | Android native SDK version | +Look for: +- New types/interfaces added +- New fields on existing types +- Breaking changes to type signatures -### 2. Type Synchronization +#### 1.3 Analyze Apple Package Changes (iOS Native) ```bash -cd $IAP_REPOS_HOME/react-native-iap +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/apple/ +``` -# Download and regenerate types (uses versions from openiap-versions.json) -yarn generate:types +Check `packages/apple/Sources/` for: +- New public functions in `OpenIapModule.swift` +- New types in `Types.swift` +- Changes to serialization -# Verify types -yarn typecheck +#### 1.4 Analyze Google Package Changes (Android Native) + +```bash +cd /Users/crossplatformkorea/Github/hyodotdev/openiap +git log -10 --oneline -- packages/google/ ``` -### 3. Native Code Modifications +Check `packages/google/openiap/src/main/` for: +- New public functions in `OpenIapModule.kt` +- New types in `Types.kt` +- Changes to Billing Library integration + +#### 1.5 Document Changes Found -#### iOS Native Code +Create a mental checklist: +- [ ] New types added? +- [ ] New API methods exposed? +- [ ] Breaking changes? +- [ ] Deprecations? +- [ ] Bug fixes? +- [ ] Platform version requirements changed? -**Location:** `ios/` +--- -Key files to update: +### Step 2: Sync openiap-versions.json (REQUIRED) -- `ios/HybridRnIap.swift` - Main iOS implementation (wraps OpenIAP Swift) -- `ios/RnIapHelper.swift` - Helper utilities -- `ios/RnIapLog.swift` - Logging utilities -- `NitroIap.podspec` - CocoaPods spec +Update react-native-iap's version tracking file to match openiap monorepo. -**When to modify:** +**Edit `openiap-versions.json`:** -- New iOS-specific API methods added to OpenIAP -- StoreKit 2 API signature changes -- Type conversion between Swift and Nitro +```json +{ + "apple": "", + "google": "", + "gql": "" +} +``` + +--- -**Update workflow:** +### Step 3: Generate Types (REQUIRED) ```bash cd $IAP_REPOS_HOME/react-native-iap -# 1. Update apple version in openiap-versions.json -# 2. Review openiap/packages/apple/Sources/ for changes -# 3. Update ios/HybridRnIap.swift accordingly -# 4. Update type-bridge.ts if new types need conversion +# Download and regenerate types +yarn generate:types -# Install updated pod -cd example/ios && bundle exec pod install --repo-update +# Review what changed +git diff src/types.ts ``` -#### Android Native Code +**Analyze the type diff carefully:** +- New interfaces/types? +- New fields on existing types? +- Changed field types? +- New enums or enum values? -**Location:** `android/src/main/java/com/margelo/nitro/iap/` +--- -Key files to update: +### Step 4: Review Native Code (REQUIRED) -- `HybridRnIap.kt` - Main Android implementation (wraps OpenIAP Kotlin) -- `RnIapLog.kt` - Logging utilities +**CRITICAL: This step catches bugs that "type-only" syncs miss. DO NOT SKIP.** -**When to modify:** +You must verify that react-native-iap's native code actually passes new options/fields to OpenIAP. -- New Android-specific API methods added to OpenIAP -- Play Billing API changes -- Type conversion between Kotlin and Nitro +#### 4.1 iOS Native Code Review -**Update workflow:** +**Location:** `ios/HybridRnIap.swift`, `ios/RnIapHelper.swift` -```bash -cd $IAP_REPOS_HOME/react-native-iap +**Verification steps:** + +1. Check what new fields were added to request props in types: + ```bash + git diff src/types.ts | grep -A5 "RequestPurchaseIosProps\|RequestSubscriptionIosProps\|PurchaseOptions" + ``` + +2. Verify react-native-iap passes these fields to OpenIAP: + ```bash + # Check if any explicit parameter passing exists that needs updating + grep -n "requestPurchase\|getAvailablePurchases" ios/HybridRnIap.swift + ``` + +**iOS typically auto-handles new fields via serialization, but verify!** -# 1. Update google version in openiap-versions.json -# 2. Review openiap/packages/google/openiap/src/main/ for changes -# 3. Update android/.../HybridRnIap.kt accordingly -# 4. Update type-bridge.ts if new types need conversion +#### 4.2 Android Native Code Review + +**Location:** `android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt` + +**Verification steps:** + +1. Check what new fields were added to Android types: + ```bash + git diff src/types.ts | grep -A5 "Android" + ``` + +2. **CRITICAL**: Check if react-native-iap's native functions pass options to OpenIAP: + ```bash + # Look for functions that might need options parameter updates + grep -n "openIap\." android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt | head -20 + ``` + +3. Verify options are forwarded: + ```bash + # Check if getAvailablePurchases receives and passes options + grep -A10 "getAvailablePurchases\|getAvailableItems" android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt + ``` + +**Android often requires explicit option passing - don't assume it works!** + +#### 4.3 TypeScript API Review + +**Location:** `src/index.ts`, `src/utils/type-bridge.ts` + +Check if TypeScript API passes new options to native: + +```bash +# Check if new options fields are included in the native call +grep -A20 "getAvailablePurchases" src/index.ts ``` -### 4. Nitro Bridge Updates +**If a new option exists in types but isn't passed in the TS/native layer, IT WON'T WORK!** + +#### 4.4 Decision Matrix (Updated) + +| Change Type | Action Required | +|-------------|-----------------| +| New types only (response types) | NO code change - OpenIAP returns them automatically | +| New INPUT option fields | **CHECK** - verify native code passes the option | +| New API function | YES - add wrapper in both native + TS | +| Breaking type change | YES - check serialization compatibility | +| New platform feature | YES - add wrapper + expose to JS | + +**Common mistakes to catch:** +- TypeScript has the option in types, but native code passes `null` instead of the options object +- Android native doesn't parse the new option from the params map +- Nitro bridge spec doesn't include new field + +--- + +### Step 5: Update API Exports (IF NEEDED) + +If new API functions were added to the native modules: + +#### 5.1 Update Nitro Bridge Spec + +**Location:** `src/specs/RnIap.nitro.ts` If types changed that affect the bridge: ```bash @@ -143,244 +259,146 @@ yarn specs yarn prepare ``` -### 5. Build & Test Native Code +#### 5.2 Update `src/index.ts` -#### iOS Build Test +Export new functions from the main entry point. -```bash -cd $IAP_REPOS_HOME/react-native-iap +#### 5.3 Update `src/hooks/useIAP.ts` -# Install dependencies -yarn install +If the new function should be available in the hook. -# Install pods for example -cd example/ios && bundle exec pod install --repo-update && cd ../.. +--- -# Build and run on simulator -yarn workspace rn-iap-example ios +### Step 6: Run All Checks (REQUIRED) -# Or build via Xcode -open example/ios/RnIapExample.xcworkspace -# Build: Cmd+B, Run: Cmd+R -``` - -#### Android Build Test +**ALL checks must pass before proceeding.** ```bash cd $IAP_REPOS_HOME/react-native-iap -# Build and run on emulator -yarn workspace rn-iap-example android - -# Or build via Android Studio -# Open example/android/ in Android Studio -# Build > Make Project -# Run > Run 'app' -``` +# TypeScript +yarn typecheck -#### Expo Example Test +# ESLint +yarn lint -```bash -cd $IAP_REPOS_HOME/react-native-iap/example-expo +# Prettier +yarn lint:prettier -bun setup -bun ios # iOS simulator -bun android # Android emulator +# Tests +yarn test ``` -#### Android Horizon Build (Meta Quest) +**If any check fails, fix before continuing.** -```bash -cd $IAP_REPOS_HOME/react-native-iap/example +--- -# Enable Horizon flavor in gradle.properties -echo "horizonEnabled=true" >> android/gradle.properties +### Step 7: Write Blog Post (REQUIRED) -# Build with Horizon -yarn android +**Every sync MUST have a blog post documenting the changes.** -# Revert for Play Store builds -sed -i '' '/horizonEnabled=true/d' android/gradle.properties -``` +#### 7.1 Create Blog Post File -### 6. Update Example Code (REQUIRED) - -**React Native Example:** `example/` - -Key screens to update: -- `example/src/screens/PurchaseFlow.tsx` - Purchase flow demo -- `example/src/screens/SubscriptionFlow.tsx` - Subscription demo -- `example/src/screens/AlternativeBilling.tsx` - Android alt billing -- `example/navigation/` - Navigation setup - -**Expo Example:** `example-expo/app/` - -**Example Code Guidelines:** -- Demonstrate ALL new API features with working code -- Show both success and error handling -- Include comments explaining the feature -- Use realistic SKU names and user flows - -**Example for new iOS feature (e.g., Win-Back Offer):** -```tsx -// In SubscriptionFlow.tsx -const handleWinBackOffer = async () => { - try { - const result = await requestSubscription({ - sku: 'premium_monthly', - winBackOffer: { offerId: 'winback_50_off' } // iOS 18+ - }); - console.log('Win-back applied:', result); - } catch (error) { - console.error('Win-back failed:', error); - } -}; -``` +**Location:** `docs/blog/` -**Example for new Android feature (e.g., Product Status):** -```tsx -// In PurchaseFlow.tsx -products.forEach((product) => { - if (product.productStatusAndroid) { - switch (product.productStatusAndroid) { - case 'OK': // Show product - break; - case 'NOT_FOUND': // Show error - break; - case 'NO_OFFERS_AVAILABLE': // Show ineligible message - break; - } - } -}); -``` +**Filename format:** `YYYY-MM-DD--.md` -### 7. Update Tests +#### 7.2 Blog Post Template -**Location:** `src/__tests__/` +```markdown +--- +slug: - +title: - +authors: [hyochan] +tags: [release, openiap, ] +date: YYYY-MM-DD +--- -```bash -yarn test # Unit tests -yarn test:all # Library + example tests -yarn test:ci # CI environment -yarn test:plugin # Expo plugin tests -``` +# Release Notes -### 8. Update Documentation (REQUIRED) +This release syncs with [OpenIAP v](https://www.openiap.dev/docs/updates/notes#). -**Location:** `docs/` -- `docs/docs/api/` - API reference -- `docs/docs/types/` - Type definitions -- `docs/docs/guides/` - Usage guides -- `docs/docs/examples/` - Code examples -- Package manager: Bun +## New Features -**Documentation Checklist:** +### ( +) -For each new feature synced from openiap: + -- [ ] **CHANGELOG.md** - Add entry for new version -- [ ] **API docs** - Function added with signature, params, return type -- [ ] **Type docs** - New types documented with all fields explained -- [ ] **Example code** - Working examples in documentation -- [ ] **Platform notes** - Version requirements (e.g., "iOS 18+", "Billing 8.0+") -- [ ] **Migration notes** - Breaking changes documented +```typescript +// Example usage +``` -**Example Documentation Entry:** -```mdx -## requestSubscription +## Bug Fixes -### Win-Back Offers (iOS 18+) +- -Win-back offers re-engage churned subscribers: +## OpenIAP Versions -```typescript -await requestSubscription({ - sku: 'premium_monthly', - winBackOffer: { offerId: 'winback_50_off' } -}); -``` +| Package | Version | +|---------|---------| +| openiap-gql | | +| openiap-google | | +| openiap-apple | | + +For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes#). ``` -### 9. Update llms.txt Files +#### 7.3 Blog Post Guidelines -**Location:** `docs/static/` +- **New features**: Explain what they do, show example code, note platform requirements +- **Breaking changes**: MUST have migration guide with before/after code +- **Type-only changes**: Still document, mention "TypeScript types updated" +- **Bug fixes**: List what was fixed +- **Always link**: Link to OpenIAP release notes -Update AI-friendly documentation files when APIs or types change: +--- -- `docs/static/llms.txt` - Quick reference for AI assistants -- `docs/static/llms-full.txt` - Detailed AI reference +### Step 8: Update llms.txt (IF API CHANGED) -**When to update:** +**Location:** `docs/static/llms.txt` and `docs/static/llms-full.txt` +Update if: - New API functions added - Function signatures changed -- New types or enums added +- New types developers need to know about - Usage patterns updated -- Error codes changed - -**Content to sync:** - -1. Installation commands -2. Core API reference (useIAP hook, direct functions) -3. Key types (Product, Purchase, ErrorCode) -4. Common usage patterns -5. Platform-specific APIs (iOS/Android suffixes) -6. Error handling examples - -### 10. Pre-commit Checklist - -```bash -yarn typecheck # TypeScript -yarn lint # ESLint -yarn lint:prettier # Prettier -yarn test # Tests -``` - -**Full Sync Checklist:** -- [ ] openiap-versions.json synced -- [ ] Types regenerated (`yarn generate:types`) -- [ ] Nitro specs regenerated if needed (`yarn specs`) -- [ ] Native code updated (iOS/Android) -- [ ] Example code demonstrates new features -- [ ] Tests pass -- [ ] Documentation updated -- [ ] llms.txt files updated +--- -### 11. Commit and Push +### Step 9: Commit and Push (REQUIRED) -After completing all sync steps, create a branch and commit the changes: +#### 9.1 Create Feature Branch ```bash cd $IAP_REPOS_HOME/react-native-iap -# Create feature branch with version number git checkout -b feat/openiap-sync- +``` -# Example: feat/openiap-sync-1.3.12 - -# Stage all changes -git add . +#### 9.2 Commit with Descriptive Message -# Commit with descriptive message -git commit -m "feat: sync with openiap v +```bash +git commit -m "$(cat <<'EOF' +feat: sync with openiap v -- Update openiap-versions.json (gql: , apple: , google: ) +- Update openiap-versions.json (gql: , apple: , google: ) - Regenerate TypeScript types -- Update example code for new types -- Update documentation and llms.txt -- Add/update tests for new features +- +- Add release blog post +- -Co-Authored-By: Claude Opus 4.5 " +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +#### 9.3 Push to Remote -# Push to remote +```bash git push -u origin feat/openiap-sync- ``` -**Branch naming conventions:** -- Feature sync: `feat/openiap-sync-` (e.g., `feat/openiap-sync-1.3.12`) -- Specific feature: `feat/` (e.g., `feat/discount-offer-types`) -- Bug fix: `fix/` (e.g., `fix/subscription-offer-parsing`) +--- ## Type Conversion Flow @@ -398,6 +416,8 @@ src/utils/type-bridge.ts (conversion functions) src/index.ts (cross-platform API) ``` +--- + ## Naming Conventions - **iOS-only:** `functionNameIOS` (e.g., `clearTransactionIOS`) @@ -406,29 +426,7 @@ src/index.ts (cross-platform API) - **iOS types:** `ProductIOS`, `PurchaseIOS` - **ID fields:** `Id` not `ID` (e.g., `productId`, `transactionId`) -## Deprecation Check - -```bash -cd $IAP_REPOS_HOME/react-native-iap -grep -r "@deprecated" src/ -grep -r "DEPRECATED" src/ -``` - -## Error Handling - -**Key files:** -- `src/utils/error.ts` - Error parsing -- `src/utils/errorMapping.ts` - Native error code mapping - -Update if `ErrorCode` enum changes in OpenIAP. - -## Commit Message Format - -``` -feat: add discount offer support -fix: resolve iOS purchase verification -docs: update subscription flow guide -``` +--- ## References From 444011191c762fb990e5f12fdec10da630a1d81c Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 18 Jan 2026 23:58:03 +0900 Subject: [PATCH 2/6] fix(gql): remove subscription-only props from RequestPurchaseIosProps Remove winBackOffer, promotionalOfferJWS, and introductoryOfferEligibility from RequestPurchaseIosProps since these options only apply to subscription products, not in-app purchases (consumables/non-consumables). These fields remain in RequestSubscriptionIosProps where they belong. Fixes gemini-code-assist review comments on expo-iap PR #305. Co-Authored-By: Claude Opus 4.5 --- packages/apple/Sources/Models/Types.swift | 21 +----------- .../src/main/java/dev/hyo/openiap/Types.kt | 27 +--------------- packages/gql/src/generated/Types.kt | 27 +--------------- packages/gql/src/generated/Types.swift | 21 +----------- packages/gql/src/generated/types.dart | 24 +------------- packages/gql/src/generated/types.gd | 32 +------------------ packages/gql/src/generated/types.ts | 21 +----------- packages/gql/src/type-ios.graphql | 23 ++----------- 8 files changed, 9 insertions(+), 187 deletions(-) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index f176fbf0..1cc151be 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1417,45 +1417,26 @@ public struct RequestPurchaseIosProps: Codable { public var andDangerouslyFinishTransactionAutomatically: Bool? /// App account token for user tracking public var appAccountToken: String? - /// Override introductory offer eligibility (iOS 15+, WWDC 2025). - /// Set to true to indicate the user is eligible for introductory offer, - /// or false to indicate they are not. When nil, the system determines eligibility. - /// Back-deployed to iOS 15. - public var introductoryOfferEligibility: Bool? - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. - public var promotionalOfferJWS: PromotionalOfferJWSInputIOS? /// Purchase quantity public var quantity: Int? /// Product SKU public var sku: String - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// Note: Win-back offers only apply to subscription products. - public var winBackOffer: WinBackOfferInputIOS? - /// Discount offer to apply + /// Discount offer to apply (one-time purchase discounts) public var withOffer: DiscountOfferInputIOS? public init( advancedCommerceData: String? = nil, andDangerouslyFinishTransactionAutomatically: Bool? = nil, appAccountToken: String? = nil, - introductoryOfferEligibility: Bool? = nil, - promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil, quantity: Int? = nil, sku: String, - winBackOffer: WinBackOfferInputIOS? = nil, withOffer: DiscountOfferInputIOS? = nil ) { self.advancedCommerceData = advancedCommerceData self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically self.appAccountToken = appAccountToken - self.introductoryOfferEligibility = introductoryOfferEligibility - self.promotionalOfferJWS = promotionalOfferJWS self.quantity = quantity self.sku = sku - self.winBackOffer = winBackOffer self.withOffer = withOffer } } diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt index b319f92f..1d1a5b89 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt @@ -3527,19 +3527,6 @@ public data class RequestPurchaseIosProps( * App account token for user tracking */ val appAccountToken: String? = null, - /** - * Override introductory offer eligibility (iOS 15+, WWDC 2025). - * Set to true to indicate the user is eligible for introductory offer, - * or false to indicate they are not. When nil, the system determines eligibility. - * Back-deployed to iOS 15. - */ - val introductoryOfferEligibility: Boolean? = null, - /** - * JWS promotional offer (iOS 15+, WWDC 2025). - * New signature format using compact JWS string for promotional offers. - * Back-deployed to iOS 15. - */ - val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null, /** * Purchase quantity */ @@ -3549,13 +3536,7 @@ public data class RequestPurchaseIosProps( */ val sku: String, /** - * Win-back offer to apply (iOS 18+) - * Used to re-engage churned subscribers with a discount or free trial. - * Note: Win-back offers only apply to subscription products. - */ - val winBackOffer: WinBackOfferInputIOS? = null, - /** - * Discount offer to apply + * Discount offer to apply (one-time purchase discounts) */ val withOffer: DiscountOfferInputIOS? = null ) { @@ -3565,11 +3546,8 @@ public data class RequestPurchaseIosProps( advancedCommerceData = json["advancedCommerceData"] as? String, andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean, appAccountToken = json["appAccountToken"] as? String, - introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean, - promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }, quantity = (json["quantity"] as? Number)?.toInt(), sku = json["sku"] as? String ?: "", - winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) }, withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) }, ) } @@ -3579,11 +3557,8 @@ public data class RequestPurchaseIosProps( "advancedCommerceData" to advancedCommerceData, "andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically, "appAccountToken" to appAccountToken, - "introductoryOfferEligibility" to introductoryOfferEligibility, - "promotionalOfferJWS" to promotionalOfferJWS?.toJson(), "quantity" to quantity, "sku" to sku, - "winBackOffer" to winBackOffer?.toJson(), "withOffer" to withOffer?.toJson(), ) } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 3a0d4987..afcfeddf 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3610,19 +3610,6 @@ public data class RequestPurchaseIosProps( * App account token for user tracking */ val appAccountToken: String? = null, - /** - * Override introductory offer eligibility (iOS 15+, WWDC 2025). - * Set to true to indicate the user is eligible for introductory offer, - * or false to indicate they are not. When nil, the system determines eligibility. - * Back-deployed to iOS 15. - */ - val introductoryOfferEligibility: Boolean? = null, - /** - * JWS promotional offer (iOS 15+, WWDC 2025). - * New signature format using compact JWS string for promotional offers. - * Back-deployed to iOS 15. - */ - val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null, /** * Purchase quantity */ @@ -3632,13 +3619,7 @@ public data class RequestPurchaseIosProps( */ val sku: String, /** - * Win-back offer to apply (iOS 18+) - * Used to re-engage churned subscribers with a discount or free trial. - * Note: Win-back offers only apply to subscription products. - */ - val winBackOffer: WinBackOfferInputIOS? = null, - /** - * Discount offer to apply + * Discount offer to apply (one-time purchase discounts) */ val withOffer: DiscountOfferInputIOS? = null ) { @@ -3648,11 +3629,8 @@ public data class RequestPurchaseIosProps( advancedCommerceData = json["advancedCommerceData"] as? String, andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean, appAccountToken = json["appAccountToken"] as? String, - introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean, - promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }, quantity = (json["quantity"] as? Number)?.toInt(), sku = json["sku"] as? String ?: "", - winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) }, withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) }, ) } @@ -3662,11 +3640,8 @@ public data class RequestPurchaseIosProps( "advancedCommerceData" to advancedCommerceData, "andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically, "appAccountToken" to appAccountToken, - "introductoryOfferEligibility" to introductoryOfferEligibility, - "promotionalOfferJWS" to promotionalOfferJWS?.toJson(), "quantity" to quantity, "sku" to sku, - "winBackOffer" to winBackOffer?.toJson(), "withOffer" to withOffer?.toJson(), ) } diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index f176fbf0..1cc151be 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1417,45 +1417,26 @@ public struct RequestPurchaseIosProps: Codable { public var andDangerouslyFinishTransactionAutomatically: Bool? /// App account token for user tracking public var appAccountToken: String? - /// Override introductory offer eligibility (iOS 15+, WWDC 2025). - /// Set to true to indicate the user is eligible for introductory offer, - /// or false to indicate they are not. When nil, the system determines eligibility. - /// Back-deployed to iOS 15. - public var introductoryOfferEligibility: Bool? - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. - public var promotionalOfferJWS: PromotionalOfferJWSInputIOS? /// Purchase quantity public var quantity: Int? /// Product SKU public var sku: String - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// Note: Win-back offers only apply to subscription products. - public var winBackOffer: WinBackOfferInputIOS? - /// Discount offer to apply + /// Discount offer to apply (one-time purchase discounts) public var withOffer: DiscountOfferInputIOS? public init( advancedCommerceData: String? = nil, andDangerouslyFinishTransactionAutomatically: Bool? = nil, appAccountToken: String? = nil, - introductoryOfferEligibility: Bool? = nil, - promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil, quantity: Int? = nil, sku: String, - winBackOffer: WinBackOfferInputIOS? = nil, withOffer: DiscountOfferInputIOS? = nil ) { self.advancedCommerceData = advancedCommerceData self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically self.appAccountToken = appAccountToken - self.introductoryOfferEligibility = introductoryOfferEligibility - self.promotionalOfferJWS = promotionalOfferJWS self.quantity = quantity self.sku = sku - self.winBackOffer = winBackOffer self.withOffer = withOffer } } diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 78c49cec..afa8bd33 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3646,11 +3646,8 @@ class RequestPurchaseIosProps { this.advancedCommerceData, this.andDangerouslyFinishTransactionAutomatically, this.appAccountToken, - this.introductoryOfferEligibility, - this.promotionalOfferJWS, this.quantity, required this.sku, - this.winBackOffer, this.withOffer, }); @@ -3663,24 +3660,11 @@ class RequestPurchaseIosProps { final bool? andDangerouslyFinishTransactionAutomatically; /// App account token for user tracking final String? appAccountToken; - /// Override introductory offer eligibility (iOS 15+, WWDC 2025). - /// Set to true to indicate the user is eligible for introductory offer, - /// or false to indicate they are not. When nil, the system determines eligibility. - /// Back-deployed to iOS 15. - final bool? introductoryOfferEligibility; - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. - final PromotionalOfferJWSInputIOS? promotionalOfferJWS; /// Purchase quantity final int? quantity; /// Product SKU final String sku; - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// Note: Win-back offers only apply to subscription products. - final WinBackOfferInputIOS? winBackOffer; - /// Discount offer to apply + /// Discount offer to apply (one-time purchase discounts) final DiscountOfferInputIOS? withOffer; factory RequestPurchaseIosProps.fromJson(Map json) { @@ -3688,11 +3672,8 @@ class RequestPurchaseIosProps { advancedCommerceData: json['advancedCommerceData'] as String?, andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?, appAccountToken: json['appAccountToken'] as String?, - introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?, - promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null, quantity: json['quantity'] as int?, sku: json['sku'] as String, - winBackOffer: json['winBackOffer'] != null ? WinBackOfferInputIOS.fromJson(json['winBackOffer'] as Map) : null, withOffer: json['withOffer'] != null ? DiscountOfferInputIOS.fromJson(json['withOffer'] as Map) : null, ); } @@ -3702,11 +3683,8 @@ class RequestPurchaseIosProps { 'advancedCommerceData': advancedCommerceData, 'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically, 'appAccountToken': appAccountToken, - 'introductoryOfferEligibility': introductoryOfferEligibility, - 'promotionalOfferJWS': promotionalOfferJWS?.toJson(), 'quantity': quantity, 'sku': sku, - 'winBackOffer': winBackOffer?.toJson(), 'withOffer': withOffer?.toJson(), }; } diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index eb3932ed..7b6d1b8e 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -3054,14 +3054,8 @@ class RequestPurchaseIosProps: var app_account_token: String ## Purchase quantity var quantity: int - ## Discount offer to apply + ## Discount offer to apply (one-time purchase discounts) var with_offer: DiscountOfferInputIOS - ## Win-back offer to apply (iOS 18+) - var win_back_offer: WinBackOfferInputIOS - ## JWS promotional offer (iOS 15+, WWDC 2025). - var promotional_offer_jws: PromotionalOfferJWSInputIOS - ## Override introductory offer eligibility (iOS 15+, WWDC 2025). - var introductory_offer_eligibility: bool ## Advanced commerce data token (iOS 15+). var advanced_commerce_data: String @@ -3080,18 +3074,6 @@ class RequestPurchaseIosProps: obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) else: obj.with_offer = data["withOffer"] - if data.has("winBackOffer") and data["winBackOffer"] != null: - if data["winBackOffer"] is Dictionary: - obj.win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"]) - else: - obj.win_back_offer = data["winBackOffer"] - if data.has("promotionalOfferJWS") and data["promotionalOfferJWS"] != null: - if data["promotionalOfferJWS"] is Dictionary: - obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"]) - else: - obj.promotional_offer_jws = data["promotionalOfferJWS"] - if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null: - obj.introductory_offer_eligibility = data["introductoryOfferEligibility"] if data.has("advancedCommerceData") and data["advancedCommerceData"] != null: obj.advanced_commerce_data = data["advancedCommerceData"] return obj @@ -3111,18 +3093,6 @@ class RequestPurchaseIosProps: dict["withOffer"] = with_offer.to_dict() else: dict["withOffer"] = with_offer - if win_back_offer != null: - if win_back_offer.has_method("to_dict"): - dict["winBackOffer"] = win_back_offer.to_dict() - else: - dict["winBackOffer"] = win_back_offer - if promotional_offer_jws != null: - if promotional_offer_jws.has_method("to_dict"): - dict["promotionalOfferJWS"] = promotional_offer_jws.to_dict() - else: - dict["promotionalOfferJWS"] = promotional_offer_jws - if introductory_offer_eligibility != null: - dict["introductoryOfferEligibility"] = introductory_offer_eligibility if advanced_commerce_data != null: dict["advancedCommerceData"] = advanced_commerce_data return dict diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index 28e59152..df3bd660 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1216,30 +1216,11 @@ export interface RequestPurchaseIosProps { andDangerouslyFinishTransactionAutomatically?: (boolean | null); /** App account token for user tracking */ appAccountToken?: (string | null); - /** - * Override introductory offer eligibility (iOS 15+, WWDC 2025). - * Set to true to indicate the user is eligible for introductory offer, - * or false to indicate they are not. When nil, the system determines eligibility. - * Back-deployed to iOS 15. - */ - introductoryOfferEligibility?: (boolean | null); - /** - * JWS promotional offer (iOS 15+, WWDC 2025). - * New signature format using compact JWS string for promotional offers. - * Back-deployed to iOS 15. - */ - promotionalOfferJWS?: (PromotionalOfferJwsInputIOS | null); /** Purchase quantity */ quantity?: (number | null); /** Product SKU */ sku: string; - /** - * Win-back offer to apply (iOS 18+) - * Used to re-engage churned subscribers with a discount or free trial. - * Note: Win-back offers only apply to subscription products. - */ - winBackOffer?: (WinBackOfferInputIOS | null); - /** Discount offer to apply */ + /** Discount offer to apply (one-time purchase discounts) */ withOffer?: (DiscountOfferInputIOS | null); } diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql index e5e3dd1c..aa20aff7 100644 --- a/packages/gql/src/type-ios.graphql +++ b/packages/gql/src/type-ios.graphql @@ -218,7 +218,7 @@ type PurchaseOfferIOS { paymentMode: String! } -# iOS purchase/subscribe inputs +# iOS purchase inputs (for in-app products: consumables and non-consumables) input RequestPurchaseIosProps { """ Product SKU @@ -237,29 +237,10 @@ input RequestPurchaseIosProps { """ quantity: Int """ - Discount offer to apply + Discount offer to apply (one-time purchase discounts) """ withOffer: DiscountOfferInputIOS """ - Win-back offer to apply (iOS 18+) - Used to re-engage churned subscribers with a discount or free trial. - Note: Win-back offers only apply to subscription products. - """ - winBackOffer: WinBackOfferInputIOS - """ - JWS promotional offer (iOS 15+, WWDC 2025). - New signature format using compact JWS string for promotional offers. - Back-deployed to iOS 15. - """ - promotionalOfferJWS: PromotionalOfferJWSInputIOS - """ - Override introductory offer eligibility (iOS 15+, WWDC 2025). - Set to true to indicate the user is eligible for introductory offer, - or false to indicate they are not. When nil, the system determines eligibility. - Back-deployed to iOS 15. - """ - introductoryOfferEligibility: Boolean - """ Advanced commerce data token (iOS 15+). Used with StoreKit 2's Product.PurchaseOption.custom API for passing campaign tokens, affiliate IDs, or other attribution data. From ee891b35e2b8b14e1502fc8aa4151ba9b7da8a92 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 19 Jan 2026 00:04:34 +0900 Subject: [PATCH 3/6] fix(apple): handle subscription-only props via protocol Introduce IosPropsProtocol to handle both RequestPurchaseIosProps and RequestSubscriptionIosProps polymorphically. Subscription-only options (winBackOffer, promotionalOfferJWS, introductoryOfferEligibility) are now only accessed when the props is RequestSubscriptionIosProps. Co-Authored-By: Claude Opus 4.5 --- .../Sources/Helpers/StoreKitTypesBridge.swift | 153 +++++++++--------- packages/apple/Sources/OpenIapModule.swift | 16 +- packages/apple/Sources/OpenIapProtocol.swift | 16 ++ 3 files changed, 99 insertions(+), 86 deletions(-) diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift index 90af35e6..4d3632f2 100644 --- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift +++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift @@ -359,7 +359,7 @@ enum StoreKitTypesBridge { } } - static func purchaseOptions(from props: RequestPurchaseIosProps, product: StoreKit.Product? = nil) throws -> Set { + static func purchaseOptions(from props: some IosPropsProtocol, product: StoreKit.Product? = nil) throws -> Set { var options: Set = [] if let quantity = props.quantity, quantity > 1 { options.insert(.quantity(quantity)) @@ -377,88 +377,93 @@ enum StoreKitTypesBridge { } options.insert(option) } - // Win-back offers (iOS 18+) - // Used to re-engage churned subscribers - if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { - if let winBackInput = props.winBackOffer { - guard let product = product else { - OpenIapLog.error("❌ Win-back offer requires product context") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Win-back offer requires product context. Fetch the product before calling requestPurchase." - ) - } - // Find the win-back offer from the product's promotional offers - if let subscription = product.subscription { - let winBackOffer = subscription.promotionalOffers.first { offer in - offer.id == winBackInput.offerId && offer.type == .winBack + + // Subscription-only options (only available on RequestSubscriptionIosProps) + if let subscriptionProps = props as? RequestSubscriptionIosProps { + // Win-back offers (iOS 18+) + // Used to re-engage churned subscribers + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + if let winBackInput = subscriptionProps.winBackOffer { + guard let product = product else { + OpenIapLog.error("❌ Win-back offer requires product context") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Win-back offer requires product context. Fetch the product before calling requestPurchase." + ) } - if let offer = winBackOffer { - options.insert(.winBackOffer(offer)) - OpenIapLog.debug("✅ Added win-back offer: \(winBackInput.offerId)") + // Find the win-back offer from the product's promotional offers + if let subscription = product.subscription { + let winBackOffer = subscription.promotionalOffers.first { offer in + offer.id == winBackInput.offerId && offer.type == .winBack + } + if let offer = winBackOffer { + options.insert(.winBackOffer(offer)) + OpenIapLog.debug("✅ Added win-back offer: \(winBackInput.offerId)") + } else { + OpenIapLog.error("❌ Win-back offer not found: \(winBackInput.offerId)") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Win-back offer not found: \(winBackInput.offerId). Ensure the user is eligible and the offer ID is correct." + ) + } } else { - OpenIapLog.error("❌ Win-back offer not found: \(winBackInput.offerId)") + OpenIapLog.error("❌ Win-back offer requires a subscription product") throw PurchaseError.make( code: .developerError, productId: props.sku, - message: "Win-back offer not found: \(winBackInput.offerId). Ensure the user is eligible and the offer ID is correct." + message: "Win-back offers can only be applied to subscription products" ) } - } else { - OpenIapLog.error("❌ Win-back offer requires a subscription product") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Win-back offers can only be applied to subscription products" - ) } + } else if subscriptionProps.winBackOffer != nil { + // Fail fast when win-back offers are used on unsupported OS versions + OpenIapLog.error("❌ Win-back offers require iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+." + ) + } + + // JWS Promotional Offer (iOS 15+, WWDC 2025) + // New signature format using compact JWS string for promotional offers + // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile + if let jwsOffer = subscriptionProps.promotionalOfferJWS { + #if swift(>=6.1) + // Swift 6.1+ implementation + options.insert(.promotionalOffer(jwsOffer.jws)) + OpenIapLog.debug("✅ Added JWS promotional offer: \(jwsOffer.offerId)") + #else + // Swift < 6.1: API not available, throw error to fail fast + OpenIapLog.error("❌ JWS promotional offers require Xcode 16.4+ / Swift 6.1+") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "JWS promotional offers require Xcode 16.4+ / Swift 6.1+. Use withOffer with signature-based promotional offers instead." + ) + #endif + } + + // Introductory Offer Eligibility Override (iOS 15+, WWDC 2025) + // Allows overriding the system's eligibility check for introductory offers + // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile + if let eligibility = subscriptionProps.introductoryOfferEligibility { + #if swift(>=6.1) + // Swift 6.1+ implementation + options.insert(.introductoryOfferEligibility(eligibility)) + OpenIapLog.debug("✅ Added introductory offer eligibility override: \(eligibility)") + #else + // Swift < 6.1: API not available, throw error to fail fast + OpenIapLog.error("❌ Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+. The system will determine eligibility automatically." + ) + #endif } - } else if props.winBackOffer != nil { - // Fail fast when win-back offers are used on unsupported OS versions - OpenIapLog.error("❌ Win-back offers require iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+." - ) - } - // JWS Promotional Offer (iOS 15+, WWDC 2025) - // New signature format using compact JWS string for promotional offers - // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile - if let jwsOffer = props.promotionalOfferJWS { - #if swift(>=6.1) - // Swift 6.1+ implementation - options.insert(.promotionalOffer(jwsOffer.jws)) - OpenIapLog.debug("✅ Added JWS promotional offer: \(jwsOffer.offerId)") - #else - // Swift < 6.1: API not available, throw error to fail fast - OpenIapLog.error("❌ JWS promotional offers require Xcode 16.4+ / Swift 6.1+") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "JWS promotional offers require Xcode 16.4+ / Swift 6.1+. Use withOffer with signature-based promotional offers instead." - ) - #endif - } - - // Introductory Offer Eligibility Override (iOS 15+, WWDC 2025) - // Allows overriding the system's eligibility check for introductory offers - // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile - if let eligibility = props.introductoryOfferEligibility { - #if swift(>=6.1) - // Swift 6.1+ implementation - options.insert(.introductoryOfferEligibility(eligibility)) - OpenIapLog.debug("✅ Added introductory offer eligibility override: \(eligibility)") - #else - // Swift < 6.1: API not available, throw error to fail fast - OpenIapLog.error("❌ Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+. The system will determine eligibility automatically." - ) - #endif } // Advanced Commerce Data (iOS 15+) diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 2741bcd2..b0ef3f98 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -1263,7 +1263,9 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { return first } - private func resolveIosPurchaseProps(from params: RequestPurchaseProps) throws -> RequestPurchaseIosProps { + /// Resolves iOS purchase props from request params. + /// Returns either RequestPurchaseIosProps or RequestSubscriptionIosProps based on request type. + private func resolveIosPurchaseProps(from params: RequestPurchaseProps) throws -> any IosPropsProtocol { switch params.request { case let .purchase(platforms): if let ios = platforms.ios { @@ -1271,17 +1273,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } case let .subscription(platforms): if let ios = platforms.ios { - return RequestPurchaseIosProps( - advancedCommerceData: ios.advancedCommerceData, - andDangerouslyFinishTransactionAutomatically: ios.andDangerouslyFinishTransactionAutomatically, - appAccountToken: ios.appAccountToken, - introductoryOfferEligibility: ios.introductoryOfferEligibility, - promotionalOfferJWS: ios.promotionalOfferJWS, - quantity: ios.quantity, - sku: ios.sku, - winBackOffer: ios.winBackOffer, - withOffer: ios.withOffer - ) + return ios } } throw makePurchaseError(code: .purchaseError, message: "Missing iOS purchase parameters") diff --git a/packages/apple/Sources/OpenIapProtocol.swift b/packages/apple/Sources/OpenIapProtocol.swift index 676a493d..8add5473 100644 --- a/packages/apple/Sources/OpenIapProtocol.swift +++ b/packages/apple/Sources/OpenIapProtocol.swift @@ -1,6 +1,22 @@ import Foundation import StoreKit +// MARK: - iOS Props Protocol + +/// Protocol for iOS purchase/subscription props to enable polymorphic handling. +/// Both RequestPurchaseIosProps and RequestSubscriptionIosProps conform to this protocol. +public protocol IosPropsProtocol { + var sku: String { get } + var quantity: Int? { get } + var appAccountToken: String? { get } + var withOffer: DiscountOfferInputIOS? { get } + var advancedCommerceData: String? { get } + var andDangerouslyFinishTransactionAutomatically: Bool? { get } +} + +extension RequestPurchaseIosProps: IosPropsProtocol {} +extension RequestSubscriptionIosProps: IosPropsProtocol {} + // MARK: - Event Listeners @available(iOS 15.0, macOS 14.0, *) From a327b3524791f0a81a2c75d916caeb34aa1f7d40 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 19 Jan 2026 00:16:42 +0900 Subject: [PATCH 4/6] fix: address PR review comments - Refactor win-back offer handling to check winBackOffer first, then #available (groups related logic in single if-let block per gemini-code-assist suggestion) - Update withOffer docstring: clarify that iOS only supports promotional offers for subscriptions, not one-time purchases (per coderabbitai review) - Regenerate types with updated documentation Co-Authored-By: Claude Opus 4.5 --- .../Sources/Helpers/StoreKitTypesBridge.swift | 20 ++++---- packages/apple/Sources/Models/Types.swift | 5 +- .../docs/src/pages/docs/updates/notes.tsx | 49 +++++++++++++++++++ .../src/main/java/dev/hyo/openiap/Types.kt | 7 ++- packages/gql/src/generated/Types.kt | 7 ++- packages/gql/src/generated/Types.swift | 5 +- packages/gql/src/generated/types.dart | 5 +- packages/gql/src/generated/types.gd | 3 +- packages/gql/src/generated/types.ts | 9 +++- packages/gql/src/type-ios.graphql | 7 ++- 10 files changed, 99 insertions(+), 18 deletions(-) diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift index 4d3632f2..69a2ac4a 100644 --- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift +++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift @@ -382,8 +382,8 @@ enum StoreKitTypesBridge { if let subscriptionProps = props as? RequestSubscriptionIosProps { // Win-back offers (iOS 18+) // Used to re-engage churned subscribers - if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { - if let winBackInput = subscriptionProps.winBackOffer { + if let winBackInput = subscriptionProps.winBackOffer { + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { guard let product = product else { OpenIapLog.error("❌ Win-back offer requires product context") throw PurchaseError.make( @@ -416,15 +416,15 @@ enum StoreKitTypesBridge { message: "Win-back offers can only be applied to subscription products" ) } + } else { + // Fail fast when win-back offers are used on unsupported OS versions + OpenIapLog.error("❌ Win-back offers require iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+." + ) } - } else if subscriptionProps.winBackOffer != nil { - // Fail fast when win-back offers are used on unsupported OS versions - OpenIapLog.error("❌ Win-back offers require iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+." - ) } // JWS Promotional Offer (iOS 15+, WWDC 2025) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 1cc151be..19407cb1 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1421,7 +1421,8 @@ public struct RequestPurchaseIosProps: Codable { public var quantity: Int? /// Product SKU public var sku: String - /// Discount offer to apply (one-time purchase discounts) + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. public var withOffer: DiscountOfferInputIOS? public init( @@ -1611,6 +1612,8 @@ public struct RequestSubscriptionIosProps: Codable { /// The offer is available when the customer is eligible and can be discovered /// via StoreKit Message (automatic) or subscription offer APIs. public var winBackOffer: WinBackOfferInputIOS? + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. public var withOffer: DiscountOfferInputIOS? public init( diff --git a/packages/docs/src/pages/docs/updates/notes.tsx b/packages/docs/src/pages/docs/updates/notes.tsx index 0f315d6a..6bc011a8 100644 --- a/packages/docs/src/pages/docs/updates/notes.tsx +++ b/packages/docs/src/pages/docs/updates/notes.tsx @@ -26,6 +26,55 @@ function Notes() { useScrollToHash(); const allNotes: Note[] = [ + // GQL 1.3.14 / Apple 1.3.12 - Jan 19, 2026 + { + id: 'gql-1-3-14-apple-1-3-12', + date: new Date('2026-01-19'), + element: ( +
+ + 📅 openiap-gql v1.3.14 / openiap-apple v1.3.12 - Subscription-Only Props Fix + + +

Type Safety Improvement:

+

+ Moved subscription-only fields from RequestPurchaseIosProps to{' '} + RequestSubscriptionIosProps only. These fields only apply to subscription purchases, + not in-app products (consumables/non-consumables). +

+ +

Fields Moved (subscription-only):

+
    +
  • winBackOffer - Win-back offers for churned subscribers (iOS 18+)
  • +
  • promotionalOfferJWS - JWS promotional offers (iOS 15+)
  • +
  • introductoryOfferEligibility - Override intro offer eligibility (iOS 15+)
  • +
+ +

Impact:

+
    +
  • No functional changes - these options were only used for subscriptions anyway
  • +
  • Better type safety - TypeScript will now error if you try to use subscription options on in-app purchases
  • +
  • Clearer API design - subscription-specific options are only available in subscription context
  • +
+ +

Migration:

+

If you were using these fields with type: 'in-app', change to type: 'subs':

+
+{`// Before (would be ignored anyway for in-app)
+requestPurchase({
+  request: { apple: { sku: 'product', winBackOffer: {...} } },
+  type: 'in-app'  // ❌ winBackOffer doesn't apply
+});
+
+// After (correct usage)
+requestPurchase({
+  request: { apple: { sku: 'subscription', winBackOffer: {...} } },
+  type: 'subs'    // ✅ winBackOffer only works with subscriptions
+});`}
+          
+
+ ), + }, // GQL 1.3.13 / Google 1.3.24 / Apple 1.3.11 - Jan 18, 2026 { id: 'gql-1-3-13-google-1-3-24-apple-1-3-11', diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt index 1d1a5b89..b1e79710 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt @@ -3536,7 +3536,8 @@ public data class RequestPurchaseIosProps( */ val sku: String, /** - * Discount offer to apply (one-time purchase discounts) + * Promotional offer to apply (subscriptions only, ignored for one-time purchases). + * iOS only supports promotional offers for auto-renewable subscriptions. */ val withOffer: DiscountOfferInputIOS? = null ) { @@ -3764,6 +3765,10 @@ public data class RequestSubscriptionIosProps( * via StoreKit Message (automatic) or subscription offer APIs. */ val winBackOffer: WinBackOfferInputIOS? = null, + /** + * Promotional offer to apply for subscription purchases. + * Requires server-signed offer with nonce, timestamp, keyId, and signature. + */ val withOffer: DiscountOfferInputIOS? = null ) { companion object { diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index afcfeddf..978e8942 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3619,7 +3619,8 @@ public data class RequestPurchaseIosProps( */ val sku: String, /** - * Discount offer to apply (one-time purchase discounts) + * Promotional offer to apply (subscriptions only, ignored for one-time purchases). + * iOS only supports promotional offers for auto-renewable subscriptions. */ val withOffer: DiscountOfferInputIOS? = null ) { @@ -3847,6 +3848,10 @@ public data class RequestSubscriptionIosProps( * via StoreKit Message (automatic) or subscription offer APIs. */ val winBackOffer: WinBackOfferInputIOS? = null, + /** + * Promotional offer to apply for subscription purchases. + * Requires server-signed offer with nonce, timestamp, keyId, and signature. + */ val withOffer: DiscountOfferInputIOS? = null ) { companion object { diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 1cc151be..19407cb1 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1421,7 +1421,8 @@ public struct RequestPurchaseIosProps: Codable { public var quantity: Int? /// Product SKU public var sku: String - /// Discount offer to apply (one-time purchase discounts) + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. public var withOffer: DiscountOfferInputIOS? public init( @@ -1611,6 +1612,8 @@ public struct RequestSubscriptionIosProps: Codable { /// The offer is available when the customer is eligible and can be discovered /// via StoreKit Message (automatic) or subscription offer APIs. public var winBackOffer: WinBackOfferInputIOS? + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. public var withOffer: DiscountOfferInputIOS? public init( diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index afa8bd33..b873b3ef 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3664,7 +3664,8 @@ class RequestPurchaseIosProps { final int? quantity; /// Product SKU final String sku; - /// Discount offer to apply (one-time purchase discounts) + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. final DiscountOfferInputIOS? withOffer; factory RequestPurchaseIosProps.fromJson(Map json) { @@ -3893,6 +3894,8 @@ class RequestSubscriptionIosProps { /// The offer is available when the customer is eligible and can be discovered /// via StoreKit Message (automatic) or subscription offer APIs. final WinBackOfferInputIOS? winBackOffer; + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. final DiscountOfferInputIOS? withOffer; factory RequestSubscriptionIosProps.fromJson(Map json) { diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 7b6d1b8e..2ea458bc 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -3054,7 +3054,7 @@ class RequestPurchaseIosProps: var app_account_token: String ## Purchase quantity var quantity: int - ## Discount offer to apply (one-time purchase discounts) + ## Promotional offer to apply (subscriptions only, ignored for one-time purchases). var with_offer: DiscountOfferInputIOS ## Advanced commerce data token (iOS 15+). var advanced_commerce_data: String @@ -3298,6 +3298,7 @@ class RequestSubscriptionIosProps: var and_dangerously_finish_transaction_automatically: bool var app_account_token: String var quantity: int + ## Promotional offer to apply for subscription purchases. var with_offer: DiscountOfferInputIOS ## Win-back offer to apply (iOS 18+) var win_back_offer: WinBackOfferInputIOS diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index df3bd660..c646d5bc 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1220,7 +1220,10 @@ export interface RequestPurchaseIosProps { quantity?: (number | null); /** Product SKU */ sku: string; - /** Discount offer to apply (one-time purchase discounts) */ + /** + * Promotional offer to apply (subscriptions only, ignored for one-time purchases). + * iOS only supports promotional offers for auto-renewable subscriptions. + */ withOffer?: (DiscountOfferInputIOS | null); } @@ -1324,6 +1327,10 @@ export interface RequestSubscriptionIosProps { * via StoreKit Message (automatic) or subscription offer APIs. */ winBackOffer?: (WinBackOfferInputIOS | null); + /** + * Promotional offer to apply for subscription purchases. + * Requires server-signed offer with nonce, timestamp, keyId, and signature. + */ withOffer?: (DiscountOfferInputIOS | null); } diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql index aa20aff7..75a77934 100644 --- a/packages/gql/src/type-ios.graphql +++ b/packages/gql/src/type-ios.graphql @@ -237,7 +237,8 @@ input RequestPurchaseIosProps { """ quantity: Int """ - Discount offer to apply (one-time purchase discounts) + Promotional offer to apply (subscriptions only, ignored for one-time purchases). + iOS only supports promotional offers for auto-renewable subscriptions. """ withOffer: DiscountOfferInputIOS """ @@ -255,6 +256,10 @@ input RequestSubscriptionIosProps { andDangerouslyFinishTransactionAutomatically: Boolean appAccountToken: String quantity: Int + """ + Promotional offer to apply for subscription purchases. + Requires server-signed offer with nonce, timestamp, keyId, and signature. + """ withOffer: DiscountOfferInputIOS """ Win-back offer to apply (iOS 18+) From 80456f143a30ce5283f3401dbd957934dc5955e6 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 19 Jan 2026 00:30:56 +0900 Subject: [PATCH 5/6] fix: use named parameter for promotionalOffer API - Changed .promotionalOffer(jwsOffer.jws) to .promotionalOffer(compactJWS: jwsOffer.jws) - Matches official Apple StoreKit API signature Co-Authored-By: Claude Opus 4.5 --- packages/apple/Sources/Helpers/StoreKitTypesBridge.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift index 69a2ac4a..14e799c1 100644 --- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift +++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift @@ -433,7 +433,7 @@ enum StoreKitTypesBridge { if let jwsOffer = subscriptionProps.promotionalOfferJWS { #if swift(>=6.1) // Swift 6.1+ implementation - options.insert(.promotionalOffer(jwsOffer.jws)) + options.insert(.promotionalOffer(compactJWS: jwsOffer.jws)) OpenIapLog.debug("✅ Added JWS promotional offer: \(jwsOffer.offerId)") #else // Swift < 6.1: API not available, throw error to fail fast From 8f1d1ddaaf1654d0dc6e29c3d1e3105fbe9267b3 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 19 Jan 2026 00:36:01 +0900 Subject: [PATCH 6/6] test(apple): add subscription-only props tests Add tests for subscription-only iOS purchase options: - testRequestSubscriptionIosPropsWithWinBackOffer - testRequestSubscriptionIosPropsWithPromotionalOfferJWS - testRequestSubscriptionIosPropsWithIntroductoryOfferEligibility - testRequestSubscriptionIosPropsWithAllSubscriptionOnlyFields - testSubscriptionPropsHasSubscriptionOnlyFields Co-Authored-By: Claude Opus 4.5 --- packages/apple/Tests/OpenIapTests.swift | 157 ++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift index 8a3af1c9..419da41f 100644 --- a/packages/apple/Tests/OpenIapTests.swift +++ b/packages/apple/Tests/OpenIapTests.swift @@ -375,6 +375,163 @@ final class OpenIapTests: XCTestCase { XCTAssertTrue(jsonString.contains("promo_code_abc")) } + // MARK: - Subscription-Only Props Tests + + func testRequestSubscriptionIosPropsWithWinBackOffer() throws { + let winBackOffer = WinBackOfferInputIOS(offerId: "winback_50_off") + let props = RequestSubscriptionIosProps( + advancedCommerceData: nil, + andDangerouslyFinishTransactionAutomatically: nil, + appAccountToken: nil, + introductoryOfferEligibility: nil, + promotionalOfferJWS: nil, + quantity: nil, + sku: "dev.hyo.subscription.monthly", + winBackOffer: winBackOffer, + withOffer: nil + ) + + XCTAssertEqual(props.sku, "dev.hyo.subscription.monthly") + XCTAssertNotNil(props.winBackOffer) + XCTAssertEqual(props.winBackOffer?.offerId, "winback_50_off") + + // Test encoding/decoding + let data = try JSONEncoder().encode(props) + let decoded = try JSONDecoder().decode(RequestSubscriptionIosProps.self, from: data) + XCTAssertEqual(decoded.winBackOffer?.offerId, "winback_50_off") + } + + func testRequestSubscriptionIosPropsWithPromotionalOfferJWS() throws { + let jwsOffer = PromotionalOfferJWSInputIOS(jws: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...", offerId: "promo_20_off") + let props = RequestSubscriptionIosProps( + advancedCommerceData: nil, + andDangerouslyFinishTransactionAutomatically: nil, + appAccountToken: nil, + introductoryOfferEligibility: nil, + promotionalOfferJWS: jwsOffer, + quantity: nil, + sku: "dev.hyo.subscription.yearly", + winBackOffer: nil, + withOffer: nil + ) + + XCTAssertEqual(props.sku, "dev.hyo.subscription.yearly") + XCTAssertNotNil(props.promotionalOfferJWS) + XCTAssertEqual(props.promotionalOfferJWS?.offerId, "promo_20_off") + XCTAssertEqual(props.promotionalOfferJWS?.jws, "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...") + + // Test encoding/decoding + let data = try JSONEncoder().encode(props) + let decoded = try JSONDecoder().decode(RequestSubscriptionIosProps.self, from: data) + XCTAssertEqual(decoded.promotionalOfferJWS?.offerId, "promo_20_off") + XCTAssertEqual(decoded.promotionalOfferJWS?.jws, "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...") + } + + func testRequestSubscriptionIosPropsWithIntroductoryOfferEligibility() throws { + let props = RequestSubscriptionIosProps( + advancedCommerceData: nil, + andDangerouslyFinishTransactionAutomatically: nil, + appAccountToken: nil, + introductoryOfferEligibility: true, + promotionalOfferJWS: nil, + quantity: nil, + sku: "dev.hyo.subscription.monthly", + winBackOffer: nil, + withOffer: nil + ) + + XCTAssertEqual(props.sku, "dev.hyo.subscription.monthly") + XCTAssertEqual(props.introductoryOfferEligibility, true) + + // Test encoding/decoding + let data = try JSONEncoder().encode(props) + let decoded = try JSONDecoder().decode(RequestSubscriptionIosProps.self, from: data) + XCTAssertEqual(decoded.introductoryOfferEligibility, true) + } + + func testRequestSubscriptionIosPropsWithAllSubscriptionOnlyFields() throws { + let winBackOffer = WinBackOfferInputIOS(offerId: "winback_offer") + let jwsOffer = PromotionalOfferJWSInputIOS(jws: "jws_token", offerId: "promo_offer") + let discountOffer = DiscountOfferInputIOS( + identifier: "offer123", + keyIdentifier: "key123", + nonce: "550e8400-e29b-41d4-a716-446655440000", + signature: "base64signature", + timestamp: 1704067200000 + ) + + let props = RequestSubscriptionIosProps( + advancedCommerceData: "campaign_data", + andDangerouslyFinishTransactionAutomatically: false, + appAccountToken: "user-uuid-123", + introductoryOfferEligibility: true, + promotionalOfferJWS: jwsOffer, + quantity: 1, + sku: "dev.hyo.subscription.premium", + winBackOffer: winBackOffer, + withOffer: discountOffer + ) + + // Verify all fields + XCTAssertEqual(props.sku, "dev.hyo.subscription.premium") + XCTAssertEqual(props.quantity, 1) + XCTAssertEqual(props.appAccountToken, "user-uuid-123") + XCTAssertEqual(props.advancedCommerceData, "campaign_data") + XCTAssertEqual(props.andDangerouslyFinishTransactionAutomatically, false) + XCTAssertEqual(props.introductoryOfferEligibility, true) + XCTAssertEqual(props.winBackOffer?.offerId, "winback_offer") + XCTAssertEqual(props.promotionalOfferJWS?.offerId, "promo_offer") + XCTAssertEqual(props.withOffer?.keyIdentifier, "key123") + + // Test round-trip encoding/decoding + let data = try JSONEncoder().encode(props) + let decoded = try JSONDecoder().decode(RequestSubscriptionIosProps.self, from: data) + + XCTAssertEqual(decoded.sku, props.sku) + XCTAssertEqual(decoded.introductoryOfferEligibility, props.introductoryOfferEligibility) + XCTAssertEqual(decoded.winBackOffer?.offerId, props.winBackOffer?.offerId) + XCTAssertEqual(decoded.promotionalOfferJWS?.offerId, props.promotionalOfferJWS?.offerId) + } + + func testSubscriptionPropsHasSubscriptionOnlyFields() { + // Verify that RequestSubscriptionIosProps has subscription-only fields + let subscriptionProps = RequestSubscriptionIosProps( + advancedCommerceData: nil, + andDangerouslyFinishTransactionAutomatically: nil, + appAccountToken: nil, + introductoryOfferEligibility: true, + promotionalOfferJWS: PromotionalOfferJWSInputIOS(jws: "jws", offerId: "offer"), + quantity: nil, + sku: "sub_sku", + winBackOffer: WinBackOfferInputIOS(offerId: "winback"), + withOffer: nil + ) + + // These fields exist on RequestSubscriptionIosProps + XCTAssertEqual(subscriptionProps.introductoryOfferEligibility, true) + XCTAssertNotNil(subscriptionProps.promotionalOfferJWS) + XCTAssertNotNil(subscriptionProps.winBackOffer) + + // RequestPurchaseIosProps does NOT have these fields (compile-time check) + // This is verified at compile time - if these fields were added to + // RequestPurchaseIosProps, the code would compile differently + let purchaseProps = RequestPurchaseIosProps( + advancedCommerceData: nil, + andDangerouslyFinishTransactionAutomatically: nil, + appAccountToken: nil, + quantity: nil, + sku: "purchase_sku", + withOffer: nil + ) + + // Verify purchase props only has common fields + XCTAssertEqual(purchaseProps.sku, "purchase_sku") + XCTAssertNil(purchaseProps.quantity) + XCTAssertNil(purchaseProps.appAccountToken) + XCTAssertNil(purchaseProps.advancedCommerceData) + XCTAssertNil(purchaseProps.withOffer) + } + func testErrorCodeJSONDecoding() throws { // Test decoding from JSON with camelCase (react-native-iap format) let jsonCamel = """