axiom-codex/skills/axiom-audit-iap/SKILL.md
Use when the user mentions in-app purchase review, IAP audit, StoreKit issues, purchase bugs, transaction problems, or subscription management.
npx skillsauth add charleswiltgen/axiom axiom-audit-iapInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert at detecting in-app purchase issues — both known anti-patterns AND missing/incomplete patterns that cause revenue loss, App Store rejections, and customer support problems.
Run a comprehensive IAP audit using 5 phases: map the IAP architecture, detect known anti-patterns, reason about what's missing, correlate compound issues, and score IAP health. Report all issues with:
Skip: *Tests.swift, *Previews.swift, */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*
Before grepping, build a mental model of the codebase's IAP approach.
Glob: **/*.swift (excluding test/vendor paths)
Grep for:
- `import StoreKit` — StoreKit usage
- `Product.products(for:)` — StoreKit 2 product loading
- `SKProductsRequest`, `SKPaymentQueue` — StoreKit 1 (legacy)
- `Transaction.updates`, `Transaction.all`, `Transaction.currentEntitlements` — StoreKit 2 lifecycle
- `SKPaymentTransactionObserver` — StoreKit 1 transaction observer
StoreKit 1 is not deprecated but is legacy — note if the codebase mixes both.
Grep for:
- `.consumable`, `.nonConsumable` — Consumable / non-consumable IAP
- `.autoRenewable`, `.nonRenewable` — Subscription types
- `SubscriptionInfo`, `subscriptionGroupID` — Subscription group usage
- `RenewalInfo`, `renewalInfo` — Renewal metadata access
Read 2-3 key IAP files to understand:
Transaction.updates listener is wired (app launch, Task lifetime).finish() is called relative to entitlement grantingVerificationResult.verified) happens before grantingWrite a brief IAP Architecture Map (5-10 lines) summarizing:
Present this map in the output before proceeding.
Run all 12 existing detection patterns. These are fast and reliable. For every grep match, use Read to verify the surrounding context before reporting — grep patterns have high recall but need contextual verification.
Pattern: Transaction handling without finish()
Search: Transaction\.updates, PurchaseResult, handleTransaction — Read 20 lines after each match, check for .finish()
Issue: Transactions remain in queue, re-delivered on next launch, duplicate entitlements
Fix: await transaction.finish() after granting entitlement
Pattern: Direct transaction use without verification
Search: for await .* in Transaction\.updates, Transaction\.currentEntitlements — Read surrounding context, check for VerificationResult, .verified, .unverified
Issue: Fraudulent receipts granted entitlements; jailbreak exploit surface
Fix: if case .verified(let transaction) = result before granting
Pattern: No long-running Transaction.updates consumer
Search: Transaction\.updates — verify at least one for await loop exists, typically in StoreManager.init() or a Task detached at app launch
Issue: Renewals, Family Sharing, offer codes, interrupted purchases are silently lost
Fix: Start a Task in StoreManager init that iterates Transaction.updates for app lifetime
Pattern: No restore path wired to UI
Search: AppStore\.sync, Transaction\.all, restorePurchases, Restore.*Purchase
Issue: Guideline 3.1.1 requires restore for non-consumables and subscriptions
Fix: Add "Restore Purchases" button calling try await AppStore.sync()
Pattern: Product.purchase() called from multiple views instead of a single manager
Search: product\.purchase, Product\.purchase — collect all files with hits
Issue: Duplicate verification logic, inconsistent error handling, harder to test
Fix: Centralize in a single StoreManager (actor or @MainActor observable)
Pattern: No .storekit file in project
Search: Glob **/*.storekit
Issue: No local testing; every IAP change requires App Store Connect round-trip
Fix: File → New → File → StoreKit Configuration File (sync with App Store Connect if available)
Pattern: No appAccountToken on PurchaseOption when server validates
Search: appAccountToken, Product\.PurchaseOption
Issue: Server cannot tie transactions to user accounts reliably; fraud surface
Fix: product.purchase(options: [.appAccountToken(user.serverUUID)])
Pattern: Subscription products used but no state lookup
Search: \.autoRenewable present, but no subscriptionStatus, SubscriptionInfo\.Status, \.subscribed, \.expired, \.inGracePeriod, \.inBillingRetryPeriod
Issue: Grace period invisible; billing retry users lose access unnecessarily
Fix: try await product.subscription?.status → handle each status case
Pattern: Randomized rewards without odds UI
Search: random, shuffle, arc4random, \.random, loot, mystery, gacha, crate, pack, reward.*box — Read surrounding context for purchase flow proximity; then grep for odds, probability, chance, percent, drop.*rate
Issue: Guideline 3.1.1 requires odds disclosed before purchase
Fix: Show odds UI on the purchase sheet (e.g., "Epic: 2%, Rare: 18%, Common: 80%")
Pattern: Subscription purchase UI without price/duration/auto-renewal terms
Search: subscribe, subscription, SubscriptionView, PaywallView, SubscriptionGroup — then grep for auto.renew, cancellation, per month, per year, /month, /year, billed, renews
Issue: Guideline 3.1.2(a) requires price, duration, auto-renewal, cancellation info visible before purchase button
Fix: Show terms block adjacent to subscribe button with all four disclosures
Pattern: Purchase errors shown as raw error or "Purchase failed"
Search: purchase.*failed, purchase.*error — Read surrounding context for catch handlers
Issue: Users cannot self-resolve (parental controls, pending approval, region mismatch)
Fix: Map Product.PurchaseError and StoreKitError to actionable messages
Pattern: StoreKit code with no test coverage
Search: Glob **/*Tests.swift — grep for StoreManager, Purchase.*Test, Transaction.*Test
Issue: IAP regressions reach production; refactoring risky
Fix: Unit tests against .storekit test file using Testing or XCTest with StoreKitTest
Using the IAP Architecture Map from Phase 1 and your domain knowledge, check for what's missing — not just what's wrong.
| Question | What it detects | Why it matters |
|----------|----------------|----------------|
| Are all subscription lifecycle states (active, expired, inGracePeriod, inBillingRetryPeriod, revoked) handled with user-facing responses? | Partial state coverage | Billing retry users silently lose access; refunded users keep entitlements |
| Is server-side receipt validation in place for high-value entitlements, or is validation purely client-side? | Weak entitlement enforcement | Jailbreak/emulator bypass grants paid features for free |
| Is introductory offer eligibility checked (Product.SubscriptionInfo.isEligibleForIntroOffer) before showing intro pricing? | Ineligible users shown intro price | Users charged full price after seeing "$0.99 first month" — refund requests and 1-star reviews |
| Are offer codes and promotional offers handled (Transaction.updates with offerType=.promotional)? | Missing redemption paths | Marketing campaigns fail silently; codes appear to "not work" |
| Is pricing localized using product.displayPrice (not hardcoded strings or manual formatting)? | Hardcoded prices | Wrong currency shown to international users → purchase abandonment and Guideline 3.1.x rejection |
| Are upgrade/downgrade/crossgrade paths within a subscription group handled (comparing product.subscription?.subscriptionPeriod across group)? | Single-tier subscription UX | Users cannot move between tiers; churn increases |
| Is Family Sharing supported (checking transaction.ownershipType == .familyShared) for non-consumables and subscriptions? | All-or-nothing family handling | Shared entitlements either granted incorrectly or blocked entirely |
| Is refund handling implemented (Transaction.updates with revocationDate, or Transaction.refundRequestSheet for self-service)? | Revoked entitlements still active | Users keep access after refund; merchant fraud score affected |
| Is the encryption export declaration (ITSAppUsesNonExemptEncryption in Info.plist) set if the app uses crypto for IAP validation? | Missing export compliance | App Store Connect submission blocked pending manual review |
For each finding, explain what's missing and why it matters. Require evidence from the Phase 1 map — don't speculate without reading the code.
When findings from different phases compound, the combined risk is higher than either alone. Bump the severity when you find these combinations:
| Finding A | + Finding B | = Compound | Severity | |-----------|------------|-----------|----------| | Missing finish() | Missing Transaction.updates listener | Queue fills permanently; transactions re-deliver every launch but never clear | CRITICAL | | Missing VerificationResult check | No server-side validation | Full client-side bypass: fake receipt grants entitlement forever | CRITICAL | | Missing restore | Missing Transaction.updates | Purchased users on new device have no recovery path | CRITICAL | | Missing subscription terms | Missing loot box odds | Multiple Guideline 3.1.x rejections in one submission | HIGH | | Scattered purchase calls | Missing tests | Every refactor risks revenue regression; no safety net | HIGH | | Missing appAccountToken | Server-side validation used | Server has no reliable way to tie transactions to users | HIGH | | Missing intro offer eligibility check | Intro pricing shown in paywall | Users charged full price — refund requests and reviews | HIGH | | Missing Family Sharing check | Non-consumables sold | Family members either over-entitled or under-entitled | MEDIUM | | Missing refund handling | Subscription entitlement gated on local state | Revoked subscriptions retain access indefinitely | HIGH |
Cross-auditor overlap notes:
security-privacy-scannertesting-auditorCalculate and present a health score:
## IAP Health Score
| Metric | Value |
|--------|-------|
| Rejection-risk patterns | N missing restore + N missing subscription terms + N missing loot box odds |
| Revenue-risk patterns | N missing finish() + N missing Transaction.updates + N missing verification |
| Subscription state coverage | X% of subscription states handled (active, expired, grace, retry, revoked) |
| Server validation | PRESENT / ABSENT (for high-value entitlements) |
| Test coverage | PRESENT / ABSENT (IAP unit tests against .storekit file) |
| **Health** | **READY / NEEDS WORK / NOT READY** |
Scoring:
# IAP Audit Results
## IAP Architecture Map
[5-10 line summary from Phase 1]
## Summary
- CRITICAL: [N] issues
- HIGH: [N] issues
- MEDIUM: [N] issues
- LOW: [N] issues
- Phase 2 (pattern detection): [N] issues
- Phase 3 (completeness reasoning): [N] issues
- Phase 4 (compound findings): [N] issues
## IAP Health Score
[Phase 5 table]
## Issues by Severity
### [SEVERITY/CONFIDENCE] [Category]: [Description]
**File**: path/to/file.swift:line
**Phase**: [2: Detection | 3: Completeness | 4: Compound]
**Issue**: What's wrong or missing
**Impact**: What happens if not fixed (revenue loss, rejection, support load)
**Fix**: Code example showing the fix
**Cross-Auditor Notes**: [if overlapping with another auditor]
## Recommendations
1. [Immediate — CRITICAL revenue and rejection risks]
2. [Short-term — subscription state coverage, intro eligibility, appAccountToken]
3. [Long-term — server-side validation, centralized architecture, test coverage]
If >50 issues in one category: Show top 10, provide total count, list top 3 files If >100 total issues: Summarize by category, show only CRITICAL/HIGH details
Transaction.finish() called inside Task with proper error handling (verify it's reached)Product.purchase() in a single StoreManager even if referenced from many views (check if the call site is the manager or the view)For implementation patterns: axiom-integration skill (skills/in-app-purchases.md)
For StoreKit 2 API reference: axiom-integration skill (skills/storekit-ref.md)
For complete IAP implementation: Launch iap-implementation agent
For security of receipt validation: Launch security-privacy-scanner agent
development
Use when building ANY watchOS app — app structure, independent apps, Watch Connectivity, Smart Stack widgets, complications, controls, RelevanceKit, background tasks, ClockKit migration.
development
Use when working with HealthKit, WorkoutKit, health data, workouts, or fitness features on iOS or watchOS. Covers permissions, queries, background delivery, custom workouts, multidevice coordination.
development
Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features.
content-media
Use when working with camera, photos, audio, haptics, ShazamKit, or Now Playing. Covers AVCaptureSession, PHPicker, PhotosPicker, AVFoundation, Core Haptics, audio recognition, MediaPlayer, CarPlay, MusicKit.