in-app-purchases
Sells a Premium unlock with expo-iap behind a PurchasePort - saved entitlement, paywall states, restore, refunds, store testing.
Skill by zakariaf
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/zakariaf-in-app-purchases
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: testing, marketing
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 26, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
- Runs as
- Node.js package
Network hosts it talks to 2
api.appstoreconnect.apple.com· found in codecodeload.github.com· found in install command
Files it touches
~/.claude/skills/in-app-purchases- Your home directory (~)
What's inside
- Skill name
in-app-purchases- Description
- Sells a Premium unlock with expo-iap behind a PurchasePort - saved entitlement, paywall states, restore, refunds, store testing. Use when touching IAP, buy, restore, paywall or Premium state. Not for ads (admob-ads-and-consent).
- License
- MIT
- Instructions
- 226 lines in SKILL.md · view on GitHub
Before installing
- Ships executable scripts
Agent Skills spec issues
- files: referenced but missing: scripts/storekit/Premium.storekit, scripts/e2e/maestro-args.ts
Files (124)
- SKILL.md
- purchase-facts.json · assets/
- shared.json · assets/
- template-placeholders.json · assets/
- create-device-services.ts · examples/
- debug-premium-link.ts · examples/
- purchase-root.tsx · examples/
- checks.md · references/
- paywall-states.md · references/
- store-and-adapter.md · references/
- store-consoles.md · references/
- testing-tiers.md · references/
- check-lib.mjs · scripts/
- check-purchase-behaviour.mjs · scripts/
- check-purchases.mjs · scripts/
- app-modules.mjs · scripts/lib/
- module-stubs.mjs · scripts/lib/
- purchase-repo.mjs · scripts/lib/
- source-scan.mjs · scripts/lib/
- expo-iap.mjs · scripts/lib/stubs/
- selftest.mjs · scripts/
- 01-buy.yaml · templates/.maestro/storekit/
- 02-refund-relaunch.yaml · templates/.maestro/storekit/
- 03-ask-to-buy.yaml · templates/.maestro/storekit/
- 04-approval-relaunch.yaml · templates/.maestro/storekit/
- 05-restore.yaml · templates/.maestro/storekit/
- 06-failure.yaml · templates/.maestro/storekit/
- 07-store-unavailable.yaml · templates/.maestro/storekit/
- expo-iap.ts · templates/__mocks__/
- in-app-purchases.json · templates/contracts/
- in-app-purchases.schema.json · templates/contracts/
- asc-client.ts · templates/scripts/asc/
- asc-credentials.ts · templates/scripts/asc/
- asc-jwt.test.ts · templates/scripts/asc/
- asc-jwt.ts · templates/scripts/asc/
- create-premium-iap.ts · templates/scripts/asc/
- iap-contract.ts · templates/scripts/asc/
- premium-iap-payloads.test.ts · templates/scripts/asc/
- premium-iap-payloads.ts · templates/scripts/asc/
- system-clock.test.ts · templates/scripts/clock/
- system-clock.ts · templates/scripts/clock/
- maestro-args.test.ts · templates/scripts/e2e/
- maestro-args.ts · templates/scripts/e2e/
- ArmTests.swift · templates/scripts/storekit/
- Premium.storekit · templates/scripts/storekit/
- add-harness.rb · templates/scripts/storekit/
- busy-note.test.ts · templates/scripts/storekit/
- busy-note.ts · templates/scripts/storekit/
- storekit-harness.entitlements · templates/scripts/storekit/
- storekit-harness.ts · templates/scripts/storekit/
- purchase-products.ts · templates/src/config/
- result.test.ts · templates/src/core/
- result.ts · templates/src/core/
- entitlement-repository.ts · templates/src/data/entitlements/
- fake-entitlement-repository.ts · templates/src/data/entitlements/
- sqlite-entitlement-repository.ts · templates/src/data/entitlements/
- sql-driver.ts · templates/src/data/
- connectivity-port.ts · templates/src/services/connectivity/
- expo-network-connectivity-adapter.test.ts · templates/src/services/connectivity/
- expo-network-connectivity-adapter.ts · templates/src/services/connectivity/
- …and 64 more
SKILL.md (first lines)
# In-app purchases
Sells one non-consumable "Premium" honestly, on iOS and Android, with no server by default: the store
verifies on the device, Premium is saved before a transaction is finished, only explicit evidence
ever revokes it, every paywall state is reachable and tested, and two scripts prove it on the app's
own code.
## Rules that must hold
1. **Save Premium before finishing:** `premium.getState().grantPremium(...)` (synchronous, written
to the entitlements table) returns ok -> the paywall's `finish-purchase` -> `port.finish(tx)`.
Never finish first, never auto-finish, and never finish when the grant failed: the paywall shows
`save-failed` and the transaction stays open. The re-check and the restore finish every owned
Android purchase marked `isUnfinished` once its grant returned ok. *Why:* StoreKit re-delivers an
unfinished transaction at the next launch, so the save is retried; a finished but unsaved purchase
is lost for good, and Google refunds a purchase left unacknowledged for 3 days.
2. **Revoke only on explicit evidence:** a verified transaction with a revocation date and no newer
valid one, saved with that date (`revokePremium(revokedAtMs)`; the reducer ignores a revoke without
a positive date). Absence, read errors and offline keep the saved Premium. *Why:* a fresh purchase
can be briefly missing from the store's list. On Android a refund only removes the purchase from
the list (no revocation date), so without a server Premium stays after an Android refund: that is
the honest default (see the server profile in references/store-and-adapter.md).
3. **Listen first:** `startPremium` subscribes to purchase events before it connects, then loads the
price, then re-checks silently through `getAvailablePurchases({ onlyIncludeActiveItemsIOS: false })`
(iOS `Transaction.all`). *Why:* the store replays unfinished and approved Ask to Buy transactions
right after connecting, and only `Transaction.all` still shows refunds.
4. **Every handler is idempotent; a failure counts only while purchasing; BUY asks the store only
when the paywall accepted the tap.** *Why:* iOS reports one failure twice (the rejection and the
listener) and re-delivers owned purchases on relaunch; a refused tap must not open a sheet.
5. **Restore tells "couldn't restore" from "nothing to restore":** any failed sync or read is
`restore-failed`; `restore-empty` only after a successful sync with no evidence, never for someone who owns Premium;
a restore whose save failed is `save-failed`. *Why:* telling a payer "no purchase found" because
the network failed is the worst answer.
6. **`expo-iap` is pinned exactly (5.8.0, the stack table), imported only by
`src/services/purchase/expo-iap-purchase-adapter.ts` with its allowlist, and the plugin entry is
the bare string `'expo-iap'`.** No `useIAP`, no auto-finishing, ever; no server API (`kitApi`,
`KitApiError`, `verifyPurchase`, `verifyPurchaseWithProvider`), plugin option or store switch
(Onside, Horizon, Fire OS, Vega, alternative billing, their `EXPO_IAP_*` variables) unless
`contracts/in-app-purchases.json` opts in. *Why:* they add a server or another billing SDK, or hide
the finishing order.
7. **Every store call resolves with a value or a `StoreFailure`; none rejects.** The adapter wraps each
vendor call in `attemptAsync`; the service reads `.ok`. *Why:* a rejection escaping the adapter
skips the order above (error-handling-typed-results' adapter boundary).
8. **One non-consumable product id, written once:** the contract's `productId` (naming-conventions'
product-id grammar, valid on both stores), the `PREMIUM_PRODUCT_ID` constant and every StoreKit
configuration agree. Price point, base territory and Family Sharing are the developer's decisions
in the contract (Family Sharing off by default: once on it can never be turned off); the StoreKit
file and the App Store Connect script read them, never a typed value. *Why:* product ids can never
be reused, and a second copy of a decision drifts.
9. **An empty product list, a failed connection or offline is "store unavailable", never an error
dialog.** The port is `withConnectivity(createExpoIapPurchaseAdapter(), connectivity.isOnline)` where
it is created; `connectPremiumReloads` reloads the store on a connectivity change (and re-checks once
back online) and re-checks on every return to the foreground. Store error texts are never shown.
*Why:* StoreKit returns `[]` instead of throwing, `startPremium` runs before the first network
report, and a simulated-offline switch reaches the store only through ConnectivityPort.
10. **Show the store's own `displayPrice`, unchanged** (`storePriceText`; no text means "store
unavailable"); catalogs use `{priceText}`; never type a price into code or catalogs, never rebuild
it from the numeric `price`. *Why:* every storefront has its own price point and currency, and it
is the text the purchase sheet shows (value-objects-money-and-units states the same rule).
11. **Every paywall state is designed and tested:** a view value, a test ID from the contract and a
reducer case each (Tier 1), the StoreKit harness before a release and after an expo-iap upgrade
(Tier 2), and a human run on TestFlight and a Play testing track (Tier 3). *Why:* the real
purchase sheets exist only in store-signed builds.
12. **All paywall text comes from the app's catalogs, in every locale.** Store-console product texts
are never shown in the app. *Why:* the consoles do not cover every app locale.
13. **Premium has one writer:** the premium store (persist before publish, state-management-zustand),
called only by the purchase service and test-only code; only the service calls the purchase port;
the paywall flow is a memory-only store; everything else reads `selectIsPremium` (ads pass it to
AdsProvider as `isPremium`, admob-ads-and-consent). The entitlements table is never in a data backup
or restore (data-export-and-restore excludes it); a new device gets Premium back through Restore.
*Why:* one path keeps ads, Settings and the paywall in step, a stray grant gives Premium away, and
an editable backup file would grant it too (`entitlement-writers`, `port-callers`).
14. **The StoreKit harness exists only in throwaway harness builds:** `get-task-allow`, `*.storekit`
and `*.xctest` never ship, StoreKit test code never enters `src/`, and the StoreKit flows live in
`.maestro/storekit/`, outside the normal E2E folders. *Why:* `get-task-allow` in a store build is a
rejection, and an unarmed build would fail those flows.
15. **Harness runs name their device and ports, and relaunch flows wait 60 s:** every Maestro call
goes through `maestroGlobalArgs` (the simulator's UDID and its own driver port), Metro gets its own
port, never `--udid` alone. *Why:* other sessions share the Mac, and StoreKit answers the launch
re-check only after its transaction-cache sync (28 s measured).
16. **Premium is never pushed with pop-ups:** quiet, declared entry points (a paywall route, a
Settings row); an in-context upsell line shows at most once per local day, never to an owner
(`upsellPrice`); Restore is reachable from the paywall and Settings. *Why:* App Review requires a
restore mechanism, and a pop-up after a failure or a line on every screen reads as a trap.
17. **Humans do the store-console steps.** Agreements, tax and banking, the app record, the price and
Family Sharing decisions, attaching the first purchase to a version, and the Tier 3 runs: ask in
plain words and wait. Never submit, never print an API key or a JWT.
## Workflow
1. Read [references/store-and-adapter.md](references/store-and-adapter.md). From the repo root run
`npm install -E expo-iap@5.8.0` (verified on 2026-10-10; re-verify with `npm view expo-iap version`)
and add the bare `'expo-iap'` to the `app.config.ts` plugins. Copy `templates/contracts/` and fill
in the developer's decisions (ask for the price point, territory and Family Sharing; never guess):
replace `__PRODUCT_ID__` and `__PRICE__` in the contract first, then the same values in
`src/config/purchase-products.ts` (step 2) and `scripts/storekit/Premium.storekit` (step 6).
2. Copy the code templates at the paths their first lines name: `templates/src/services/purchase/`,
`templates/src/stores/premium/`, `templates/src/stores/paywall/`,
`templates/src/data/entitlements/`, `templates/src/config/purchase-products.ts`,
`templates/test/`, `templates/__mocks__/expo-iap.ts`. Prerequisites: `src/core/result.ts`, the data
layer with the `entitlements` table (persistence-and-migrations), ConnectivityPort
(networking-and-server-state). Add `entitlements: createSqliteEntitlementRepository(...)` to
`AppData` and keep `entitlements` in `contracts/persistence.json` `protectedTables`. The premium
store persists, so an app whose only persisting store was settings now needs
state-management-zustand's `contracts/stores.json`: add it when missing. Copy
`templates/src/stores/premium/upsell-line.ts` (+ test) only when the app shows an in-context upsell.
3. Read [references/paywall-states.md](references/paywall-states.md). Add `connectivity` and the gated
`purchase` port to the composition root as in
[examples/create-device-services.ts](examples/create-device-services.ts) (fakes in
`create-test-services.ts`), then render the startup piece of
[examples/purchase-root.tsx](examples/purchase-root.tsx) inside AppRoot: `openPurchaseStores` in a
`useState` initializer, `startPremium` after the first frame (never awaited, its unsubscribe
dropped even when the effect was cleaned up first), `connectPremiumReloads` right after it,
`PurchaseStoresProvider` around the root stack, as one wrapper so AppRoot stays within
`react/jsx-max-depth`. Build the paywall screen with screen-contracts: a
thin route (`src/app/(app)/paywall.tsx`), a model hook reading `paywallView`, `paywallNotice` and
`selectIsPremium`, and the contract's test IDs. Give every catalog the contract's keys.
4. Run Tier 1: `npx jest src/services/purchase src/stores/premium src/stores/paywall test/sqlite/sqlite-entitlement-repository.test.ts scripts/asc scripts/storekit --ci --maxWorkers=2`.
5. Run `node ${CLAUDE_SKILL_DIR}/scripts/check-purchases.mjs .` andFrom zakariaf/ReactNative-Skills, MIT.
More from zakariaf/ReactNative-Skills
- 0.606score
debug-menu-and-test-hooks
zakariaf
Gates test-only code behind one literal TEST_ONLY switch: debug menu, debug link, simulated clock and offline.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
accessibility-as-code
zakariaf
Enforces a11y labels, 44pt targets, 200% text, VoiceOver, TalkBack.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
adaptive-layout
zakariaf
Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
admob-ads-and-consent
zakariaf
Builds AdMob ads behind a port - banners, interstitials, rewarded, UMP consent then ATT, pacing, ADS_MODE test IDs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
analytics-and-consent
zakariaf
Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
android-release-play
zakariaf
Ships Android App Bundles to Google Play: upload key, Play App Signing, AAB gate, versionCode, tracks, rollout.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
architecture-and-boundaries
zakariaf
Places app code in the src/ layout and checks import boundaries: one-way zones, pure core, ports with fakes, one composition root.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
async-safety
zakariaf
Prevents silent async bugs: effect races, stale results, lost promises, leaked listeners and timers, AbortSignal gaps.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
audio-and-haptics-feedback
zakariaf
Builds SFX and haptics behind ports.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
auth-and-secure-storage
zakariaf
Builds sign-in - tokens in expo-secure-store, shared refresh, protected routes, OAuth PKCE, Apple sign-in.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
background-tasks
zakariaf
Builds expo-background-task jobs: idempotent, budgeted runs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
canvas-gestures-and-hit-testing
zakariaf
Turns canvas taps, swipes, drags and aim into typed intents: one gesture module, hit-testing on the drawn layout.
SkillAFreshness: A★ 11Claude Code +9
Signals
- Stars
- 11
- Forks
- 2
- Open issues
- 0
- Last commit (this folder)
- Oct 17, 2026 (12 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/zakariaf/ReactNative-Skills
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from zakariaf
- 0.606score
admob-ads-and-consent
zakariaf
Builds AdMob ads behind a port - banners, interstitials, rewarded, UMP consent then ATT, pacing, ADS_MODE test IDs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
accessibility-as-code
zakariaf
Enforces a11y labels, 44pt targets, 200% text, VoiceOver, TalkBack.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
canvas-gestures-and-hit-testing
zakariaf
Turns canvas taps, swipes, drags and aim into typed intents: one gesture module, hit-testing on the drawn layout.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
adaptive-layout
zakariaf
Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
debug-menu-and-test-hooks
zakariaf
Gates test-only code behind one literal TEST_ONLY switch: debug menu, debug link, simulated clock and offline.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
analytics-and-consent
zakariaf
Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT.
SkillAFreshness: A★ 11Claude Code +9
Cite this page
in-app-purchases skill. Agent Mart. https://agentmart.dev/skills/zakariaf-in-app-purchases (last updated 2026-10-24)