Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Storekit2 Iap Defaults

ASecurity

Default StoreKit 2 architecture for a single non-consumable IAP (Remove Ads, Pro Unlock): `StoreKitBridge` isolates `import StoreKit` to one Live file; launch-time `Transaction.updates`; `Transaction.currentEntitlements` for unlock state; `finish()` timing; `AppStore.sync()` restore; `.storekit` + Fake-bridge test seam. Invoke when adding IAP, wiring StoreKit 2, or asked "how do I unlock a purchase / restore purchases / test IAP". Does NOT cover subscriptions → apple-skills:storekit, or ad SD...

18 stars
0 votes
0 copies
0 views
Added 9/19/2026
developmentswifttestingapidocumentation

Works with

cliapi

Security Analysis

A100/100

Scanned 9/19/2026

Install to Claude Code

$npx -y skills add wei18/apple-dev-skills --skill storekit2-iap-defaults --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Storekit2 Iap Defaults?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Storekit2 Iap Defaults
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/wei18-storekit2-iap-defaults/badge)](https://www.skillsdirectory.com/skills/wei18-storekit2-iap-defaults)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: storekit2-iap-defaults
description: 'Default StoreKit 2 architecture for a single non-consumable IAP (Remove Ads, Pro Unlock): `StoreKitBridge` isolates `import StoreKit` to one Live file; launch-time `Transaction.updates`; `Transaction.currentEntitlements` for unlock state; `finish()` timing; `AppStore.sync()` restore; `.storekit` + Fake-bridge test seam. Invoke when adding IAP, wiring StoreKit 2, or asked "how do I unlock a purchase / restore purchases / test IAP". Does NOT cover subscriptions → apple-skills:storekit, or ad SDKs → monetization-sdk-integration.'
---

# StoreKit 2 IAP Defaults

Default shape for the smallest IAP most solo/small apps ship: one
non-consumable unlock (Remove Ads, Pro Unlock). `Product`, `Transaction`, and
`AppStore` have no public initializers — you cannot construct a fixture — so
the seam below exists to make StoreKit 2 testable at all, not for abstraction's
sake.

## When to invoke

- Adding a first non-consumable IAP to a new or existing app.
- Wiring `Product.products(for:)`, `Transaction.updates`,
  `Transaction.currentEntitlements`, or `AppStore.sync()`.
- Deciding where entitlement state lives, when to call `finish()`, or how to
  implement Restore Purchases.
- Setting up a `.storekit` configuration file or a StoreKit unit-test seam.
- Asked "how do I test a purchase without a sandbox account" or "why isn't my
  unlock surviving reinstall".

## Scope

Owns: bridge/seam shape, entitlement-derivation rules, test strategy for
**non-consumable IAP**. Does NOT own:

- Subscriptions/consumables — different renewal semantics; this skill's
  `currentEntitlements()` shape is deliberately "own it or don't."
- Ad SDK isolation — same bridge-protocol *pattern*, different domain →
  `monetization-sdk-integration`.
- What App Review requires of Restore Purchases / IAP pricing clarity (3.1.1)
  → `app-store-review-rejections`.
- Creating the IAP product in App Store Connect — the ASC API 2.0 has
  `POST /v2/inAppPurchases` plus `inAppPurchaseLocalizations`,
  `inAppPurchasePriceSchedules`, and `inAppPurchaseSubmissions` for
  automating this end-to-end → `asc-api-automation`; the web UI is the
  manual alternative.
- Getting the binary containing this code to TestFlight →
  `local-archive-export-upload`.

## The bridge seam

`Product`/`Transaction`/`AppStore` are untestable globals. Put a protocol
between the client and StoreKit; tests inject a fake instead:

```swift
// StoreKitBridge.swift — no `import StoreKit`; fully fake-able.
protocol StoreKitBridge: Sendable {
    func products(for ids: Set<String>) async throws -> [BridgeProduct]
    func currentEntitlements() async -> Set<String>
    func purchase(productId: String) async throws -> BridgePurchaseOutcome
    func sync() async throws
    func transactionUpdates() -> AsyncStream<BridgeTransactionEvent>
}
struct BridgeProduct: Sendable, Equatable { let id, displayName, displayPrice: String }
enum BridgePurchaseOutcome: Sendable, Equatable {
    case success(productId: String), userCancelled, pending, failed(reason: String)
}

// LiveStoreKitBridge.swift — the ONLY file that imports StoreKit.
import StoreKit
struct LiveStoreKitBridge: StoreKitBridge {
    func currentEntitlements() async -> Set<String> {
        var ids: Set<String> = []
        for await result in Transaction.currentEntitlements {
            guard case .verified(let t) = result, t.revocationDate == nil else { continue }
            ids.insert(t.productID)
        }
        return ids
    }
    // products(for:) / purchase(productId:) / sync() / transactionUpdates()
    // follow the same shape: Product.products(for:), Product.purchase(options:)
    // (visionOS: purchase(confirmIn:options:) instead — purchase(options:)
    // isn't available there), AppStore.sync(), Transaction.updates.
}
```

Everything above `LiveStoreKitBridge` talks only to `any StoreKitBridge` — zero
`import StoreKit`. Verify: `rg '^(internal |public )*import StoreKit' Sources/`
→ expect exactly 1 hit.

## Entitlement state, `finish()`, restore

- **Unlock state is derived, not stored.** Don't persist "isPurchased"
  independently — derive it from `currentEntitlements()` each time (a
  non-consumable with `revocationDate == nil` is entitled); an independently
  stored boolean drifts from Apple's record on refund/family-share/restore.
- **Call `finish()` after the entitlement is applied**, not before (risks
  losing the unlock on a mid-purchase crash) and not never (an unfinished
  transaction is redelivered via `Transaction.updates` on every launch).
- **The `Transaction.updates` listener starts at app launch**, not lazily on
  first paywall visit — refunds/family-share revocations/Ask-to-Buy approvals
  can arrive while the user is anywhere in the app.
- **`restorePurchases()` always calls `AppStore.sync()` first**, even when a
  local cache looks empty — Apple's `sync()` docs position it as the forced
  sync behind a user-initiated Restore Purchases control (Guideline 3.1.1
  itself only asks for a restore mechanism), so it's not an optimization to
  skip:

```swift
func restorePurchases() async throws -> [BridgeProduct] {
    try await bridge.sync()
    let entitled = await bridge.currentEntitlements()
    guard !entitled.isEmpty else { return [] }
    return try await bridge.products(for: entitled)
}
```

## Testing strategy

| Layer | Tool | Covers |
|---|---|---|
| Unit tests | `FakeStoreKitBridge` (scripted outcomes + call counters) | Client logic — zero StoreKit dependency, runs in CI |
| Interactive local run | `.storekit` file wired into the Xcode scheme (Run → Options → StoreKit Configuration) | Manual purchase-flow smoke test, no sandbox Apple ID |
| Automated purchase-flow tests | `StoreKitTest`'s `SKTestSession` (loads the same `.storekit` file) | XCUITest/integration-level flows against the real StoreKit stack |

A `.storekit` file is a testing fixture with no effect on a shipped build;
it's what enables the last two rows, not a gap in unit-test coverage if absent.

## War stories (evidence tier in italics)

- `Product.products(for:)` returns `[]`, not a thrown error, for an unknown ID
  (typo/sandbox drift) — decide what "no products" means for `purchase()`
  before you hit it (one real app: `.failed(reason: "product not found: <id>")`
  rather than a silent no-op). *Practice observed.*
- A verified `Transaction.updates`/purchase-path switch on
  `Product.PurchaseResult` needs `@unknown default`, not a plain `default` —
  without it Swift 6 mode fails to compile ("switch covers known cases, but
  'Product.PurchaseResult' may have additional unknown values"; a warning in
  Swift 5 mode), and a plain `default` would silence the warning a case Apple
  adds later should raise ([Switching Over Future Enumeration
  Cases](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/statements/#Switching-Over-Future-Enumeration-Cases));
  recheck on every OS-support bump. *Compiled-verified* (Swift 6.3.2).
- Post-purchase catalog refetch can come back empty even though the purchase
  succeeded (rare ASC catalog instability). Synthesizing a minimal entitled
  product (id + a locale-neutral placeholder price) beats `.failed` for a
  purchase Apple already charged for; pair it with a telemetry hook so the
  desync is observable. *Practice observed.*
- The transaction-observer `Task`'s priority is a real UX decision: a
  refund/family-share event should flip entitlement state promptly while the
  user may be in-session. `.background` deprioritizes it behind arbitrary
  work; one real app shipped `.background` first and upgraded to `.utility`
  after review. *Practice observed.*

Provenance for the bridge/skeleton's verification claims: `references/official-docs.md`.

## Rationale

The bridge exists because `Product`/`Transaction` have no public
initializers — "test the untestable" is the first wall a from-scratch
StoreKit 2 implementation hits, not a hypothetical. Isolating `import
StoreKit` to one file also keeps the client testable on CI runners without a
signed-in sandbox tester.

## Deviation considerations

- **A small catalog of non-consumables** — extend `BridgeProduct`'s fields,
  but keep `currentEntitlements()` a flat `Set<String>`.
- **Subscriptions** — the "own it or don't" model is too coarse; you need
  `Transaction.subscriptionStatus` and renewal-state handling this skill does
  not cover.

## Common Mistakes

1. Persisting `isPurchased` instead of deriving it from `currentEntitlements()`.
2. Never calling `finish()`, or calling it before the entitlement is applied.
3. Starting the `Transaction.updates` listener lazily instead of at launch.
4. Treating `products(for:)` returning `[]` as a thrown-error case.
5. Skipping `AppStore.sync()` in `restorePurchases()` "because the cache is empty."
6. No `@unknown default` on the `Product.PurchaseResult` switch (Swift 6 mode
   won't compile it), or a plain `default` that hides cases Apple adds later.
7. Treating the `.storekit` file as unit-test infrastructure — it configures
   the interactive runtime and `StoreKitTest`, not the fake bridge.

## Review Checklist

- [ ] `import StoreKit` appears in exactly one file.
- [ ] Unlock state is derived from `currentEntitlements()`, not stored as an
      independent boolean.
- [ ] `Transaction.updates` listener starts at app launch.
- [ ] `finish()` is called after the entitlement is applied, on every path.
- [ ] `restorePurchases()` always calls `sync()` before reading entitlements.
- [ ] `Product.PurchaseResult`'s switch has an `@unknown default` arm (not a
      plain `default`).
- [ ] A fake bridge covers purchase success/cancel/pending/failed and restore
      empty/non-empty in unit tests.
- [ ] A visible Restore Purchases control exists — this catalog's default
      places it in Settings (3.1.1 — `app-store-review-rejections`).

## Related skills

- `monetization-sdk-integration` — same bridge-isolation pattern for ad SDKs.
- `app-store-review-rejections` — Restore Purchases / pricing-clarity review gate (3.1.1).
- `asc-api-automation` — TestFlight/App Store ops after the build exists.
- `local-archive-export-upload` / `xcode-cloud-single-track-ci` — shipping the binary.
- `swift-dependency-injection` — the general protocol-injection pattern this bridge instantiates.
- `swift-testing-baseline` — where this bridge's fake fits this catalog's test stack.
- `apple-skills:storekit` (aggregated external) — StoreKit 2 API reference incl. subscriptions, `SubscriptionStoreView`, renewal state — the part this skill does not cover.
- Official sources: when verifying or updating a factual or version-sensitive claim, read `references/official-docs.md`.

Attribution

wei18wei18
View sourceMore from wei18 →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →