premium-purchase
Builds and checks each game's one Premium purchase - expo-iap behind PurchasePort, S12 state reducer, save-before-finish, pending, restore, refund revocation, store price, StoreKit harness, App Store and Play products. Use when touching IAP, buy, restore or Premium state. Not for ads (admob-ads).
- License 许可
- MIT
- 最近更新
- 2026-11-05
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- premium-purchase
- description
- Builds and checks each game's one Premium purchase - expo-iap behind PurchasePort, S12 state reducer, save-before-finish, pending, restore, refund revocation, store price, StoreKit harness, App Store and Play products. Use when touching IAP, buy, restore or Premium state. Not for ads (admob-ads).
02 资源文件 226
- 资产文件assets/premium-facts.json6.3 KB
- 资产文件assets/shared.json1.5 KB
- 参考文档examples/wire-premium.md10.2 KB
- 参考文档references/app-store-connect.md7.4 KB
- 参考文档references/google-play-billing.md10.3 KB
- 参考文档references/premium-states.md17.4 KB
- 参考文档references/store-and-adapter.md10.7 KB
- 参考文档references/testing-tiers.md19.1 KB
- 脚本scripts/check-lib.mjs44.4 KB
- 脚本scripts/check-premium-behaviour.mjs39.3 KB
- 脚本scripts/check-premium.mjs30.8 KB
- 脚本scripts/lib/assemble-fixtures.mjs3.1 KB
- 脚本scripts/lib/load-ts.mjs2.6 KB
- 脚本scripts/lib/repo-scan.mjs5.6 KB
- 脚本scripts/lib/stubs/expo-iap.mjs3.9 KB
- 脚本scripts/selftest.mjs1.5 KB
- 脚本templates/__mocks__/expo-iap.ts1.2 KB
- 参考文档templates/packages/shell/e2e/storekit/01-buy.yaml575 B
- 参考文档templates/packages/shell/e2e/storekit/02-refund-relaunch.yaml705 B
- 参考文档templates/packages/shell/e2e/storekit/03-ask-to-buy.yaml547 B
- 参考文档templates/packages/shell/e2e/storekit/04-approval-relaunch.yaml710 B
- 参考文档templates/packages/shell/e2e/storekit/05-restore.yaml828 B
- 参考文档templates/packages/shell/e2e/storekit/06-failure.yaml543 B
- 参考文档templates/packages/shell/e2e/storekit/07-store-unavailable.yaml568 B
- 参考文档templates/packages/shell/e2e/subflows/debug-setup.yaml2.3 KB
- 脚本templates/packages/shell/src/services/purchase/connectivity-gated-purchase.test.ts2.4 KB
- 脚本templates/packages/shell/src/services/purchase/connectivity-gated-purchase.ts1.1 KB
- 脚本templates/packages/shell/src/services/purchase/expo-iap-purchase-adapter.android.test.ts8.2 KB
- 脚本templates/packages/shell/src/services/purchase/expo-iap-purchase-adapter.ts4.9 KB
- 脚本templates/packages/shell/src/services/purchase/fake-purchase.ts1.3 KB
- 脚本templates/packages/shell/src/services/purchase/format-store-price.test.ts897 B
- 脚本templates/packages/shell/src/services/purchase/format-store-price.ts694 B
- 脚本templates/packages/shell/src/services/purchase/premium-service.test.ts5.6 KB
- 脚本templates/packages/shell/src/services/purchase/premium-service.ts5.5 KB
- 脚本templates/packages/shell/src/services/purchase/premium-store-flow.ts2.2 KB
- 脚本templates/packages/shell/src/services/purchase/purchase-port.ts2.1 KB
- 脚本templates/packages/shell/src/stores/premium/entitlement-evidence.test.ts1.5 KB
- 脚本templates/packages/shell/src/stores/premium/entitlement-evidence.ts1.4 KB
- 脚本templates/packages/shell/src/stores/premium/premium-flow.android.test.ts6.7 KB
- 脚本templates/packages/shell/src/stores/premium/premium-notice.test.ts2.8 KB
- 脚本templates/packages/shell/src/stores/premium/premium-notice.ts888 B
- 脚本templates/packages/shell/src/stores/premium/premium-nudge.test.ts970 B
- 脚本templates/packages/shell/src/stores/premium/premium-nudge.ts903 B
- 脚本templates/packages/shell/src/stores/premium/premium-reducer.test.ts11.1 KB
- 脚本templates/packages/shell/src/stores/premium/premium-reducer.ts2.5 KB
- 脚本templates/packages/shell/src/stores/premium/premium-state.ts2.6 KB
- 脚本templates/packages/shell/src/stores/premium/premium-store.test.ts907 B
- 脚本templates/packages/shell/src/stores/premium/premium-store.ts1.3 KB
- 脚本templates/packages/shell/src/stores/premium/premium-transitions.test.ts1.9 KB
- 脚本templates/packages/shell/src/stores/premium/premium-transitions.ts6.0 KB
- 脚本templates/packages/shell/src/stores/premium/premium-view.ts1.6 KB
- 脚本templates/packages/tooling/src/asc/asc-client.ts1.9 KB
- 脚本templates/packages/tooling/src/asc/asc-credentials.ts1.1 KB
- 脚本templates/packages/tooling/src/asc/asc-jwt.ts1.1 KB
- 脚本templates/packages/tooling/src/asc/create-premium-iap.ts1.2 KB
- 脚本templates/packages/tooling/src/asc/find-app.test.ts2.9 KB
- 脚本templates/packages/tooling/src/asc/find-app.ts2.1 KB
- 脚本templates/packages/tooling/src/asc/premium-iap-payloads.test.ts2.5 KB
- 脚本templates/packages/tooling/src/asc/premium-iap-payloads.ts3.9 KB
- 脚本templates/packages/tooling/src/asc/premium-iap-setup.test.ts3.9 KB
- 脚本templates/packages/tooling/src/asc/premium-iap-setup.ts5.1 KB
- 脚本templates/packages/tooling/src/clock/system-clock.ts584 B
- 脚本templates/packages/tooling/src/e2e/maestro-args.test.ts2.4 KB
- 脚本templates/packages/tooling/src/e2e/maestro-args.ts3.2 KB
- 脚本templates/packages/tooling/src/storekit/add-harness.rb2.0 KB
- 其他templates/packages/tooling/src/storekit/ArmTests.swift1.8 KB
- 脚本templates/packages/tooling/src/storekit/busy-note.test.ts629 B
- 脚本templates/packages/tooling/src/storekit/busy-note.ts791 B
- 脚本templates/packages/tooling/src/storekit/metro-session.test.ts2.2 KB
- 脚本templates/packages/tooling/src/storekit/metro-session.ts1.7 KB
- 其他templates/packages/tooling/src/storekit/Premium.storekit.template616 B
- 其他templates/packages/tooling/src/storekit/storekit-harness.entitlements224 B
- 脚本templates/packages/tooling/src/storekit/storekit-harness.ts8.8 KB
- 其他tests/fixtures/base-repo/.claude/skills/premium-purchase/templates/packages/tooling/src/storekit/Premium.storekit19 B
- 脚本tests/fixtures/base-repo/apps/demo-game/game.config.ts216 B
- 参考文档tests/fixtures/base-repo/apps/demo-game/package.json99 B
- 参考文档tests/fixtures/base-repo/package.json189 B
- 参考文档tests/fixtures/base-repo/packages/shell/package.json115 B
- 脚本tests/fixtures/base-repo/packages/shell/src/app/connect-premium-reloads.ts1.2 KB
- 脚本tests/fixtures/base-repo/packages/shell/src/app/create-purchase-port.ts533 B
- 脚本tests/fixtures/base-repo/packages/shell/src/app/parity/parity-purchase.ts243 B
- 脚本tests/fixtures/base-repo/packages/shell/src/config/shell-plugins.ts244 B
- 参考文档tests/fixtures/base-repo/packages/shell/src/i18n/catalogs/ckb.json2.2 KB
- 参考文档tests/fixtures/base-repo/packages/shell/src/i18n/catalogs/de.json1.7 KB
- 参考文档tests/fixtures/base-repo/packages/shell/src/i18n/catalogs/en.json1.6 KB
- 参考文档tests/fixtures/base-repo/packages/shell/src/i18n/catalogs/fa.json2.2 KB
- 脚本tests/fixtures/base-repo/packages/shell/src/screens/premium/premium-state-ids.ts841 B
- 脚本tests/fixtures/base-repo/packages/tooling/src/i18n/verify-catalogs.ts153 B
- 其他tests/fixtures/base-repo/skills/ios-release-testflight/templates/packages/tooling/src/storekit/storekit-harness.entitlements114 B
- 脚本tests/fixtures/base-repo/skills/premium-purchase/tests/fixtures/bad/apps/demo-game/harness.ts102 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-deferred-as-failed/EXPECT.txt49 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-deferred-as-failed/mutation.json231 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-drops-revocation/EXPECT.txt73 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-drops-revocation/mutation.json210 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-ignores-acknowledged/EXPECT.txt79 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-ignores-acknowledged/mutation.json211 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-restore-rethrows/EXPECT.txt81 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-restore-rethrows/mutation.json383 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-restore-swallows-failure/EXPECT.txt53 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-adapter-restore-swallows-failure/mutation.json230 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-android-pending-granted/EXPECT.txt107 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-android-pending-granted/mutation.json298 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-android-query-swallowed/EXPECT.txt99 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-android-query-swallowed/mutation.json345 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-android-unknown-revokes/EXPECT.txt97 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-android-unknown-revokes/mutation.json495 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-display-price-only/EXPECT.txt22 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-display-price-only/mutation.json230 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-duplicate-failure/EXPECT.txt45 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-duplicate-failure/mutation.json323 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-empty-after-failed-sync/EXPECT.txt46 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-empty-after-failed-sync/mutation.json329 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-empty-product-silent/EXPECT.txt56 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-empty-product-silent/mutation.json297 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-finish-first/EXPECT.txt55 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-finish-first/mutation.json764 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-finish-unsaved/EXPECT.txt66 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-finish-unsaved/mutation.json168 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-gate-connects-offline/EXPECT.txt49 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-gate-connects-offline/mutation.json231 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-listen-late/EXPECT.txt55 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-listen-late/mutation.json440 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-missing-module/EXPECT.txt51 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-missing-module/mutation.json87 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-no-foreground-recheck/EXPECT.txt49 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-no-foreground-recheck/mutation.json68 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-no-restore-toast/EXPECT.txt46 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-no-restore-toast/mutation.json206 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-no-store-reload/EXPECT.txt57 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-no-store-reload/mutation.json224 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-nudge-every-result/EXPECT.txt35 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-nudge-every-result/mutation.json231 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-read-after-failed-sync/EXPECT.txt32 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-read-after-failed-sync/mutation.json246 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-recheck-leaves-unfinished/EXPECT.txt72 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-recheck-leaves-unfinished/mutation.json339 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-reload-no-recheck/EXPECT.txt52 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-reload-no-recheck/mutation.json254 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-reload-while-buying/EXPECT.txt59 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-reload-while-buying/mutation.json281 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-restore-shows-thanks/EXPECT.txt73 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-restore-shows-thanks/mutation.json302 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-revoke-on-absence/EXPECT.txt39 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-revoke-on-absence/mutation.json383 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-revoke-without-date/EXPECT.txt47 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-revoke-without-date/mutation.json256 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-revoked-beats-owned/EXPECT.txt43 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-revoked-beats-owned/mutation.json611 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-thanks-at-launch/EXPECT.txt63 B
- 参考文档tests/fixtures/check-premium-behaviour/bad-thanks-at-launch/mutation.json218 B
- 参考文档tests/fixtures/check-premium-behaviour/good/CASE.txt59 B
- 参考文档tests/fixtures/check-premium/bad-active-only/EXPECT.txt47 B
- 参考文档tests/fixtures/check-premium/bad-active-only/mutation.json216 B
- 参考文档tests/fixtures/check-premium/bad-android-ack-in-listener/EXPECT.txt112 B
- 参考文档tests/fixtures/check-premium/bad-android-ack-in-listener/mutation.json323 B
- 参考文档tests/fixtures/check-premium/bad-android-consumed/EXPECT.txt97 B
- 参考文档tests/fixtures/check-premium/bad-android-consumed/mutation.json322 B
- 参考文档tests/fixtures/check-premium/bad-android-kotlin-version/EXPECT.txt67 B
- 参考文档tests/fixtures/check-premium/bad-android-kotlin-version/mutation.json176 B
- 参考文档tests/fixtures/check-premium/bad-android-no-kotlin/EXPECT.txt74 B
- 参考文档tests/fixtures/check-premium/bad-android-no-kotlin/mutation.json199 B
- 参考文档tests/fixtures/check-premium/bad-android-pending-as-purchased/EXPECT.txt92 B
- 参考文档tests/fixtures/check-premium/bad-android-pending-as-purchased/mutation.json298 B
- 参考文档tests/fixtures/check-premium/bad-auto-finish/EXPECT.txt58 B
- 参考文档tests/fixtures/check-premium/bad-auto-finish/mutation.json327 B
- 参考文档tests/fixtures/check-premium/bad-catalog-missing-key/EXPECT.txt74 B
- 参考文档tests/fixtures/check-premium/bad-catalog-missing-key/mutation.json161 B
- 参考文档tests/fixtures/check-premium/bad-catalog-typed-price/EXPECT.txt55 B
- 参考文档tests/fixtures/check-premium/bad-catalog-typed-price/mutation.json181 B
- 参考文档tests/fixtures/check-premium/bad-catalogs-missing-once-due/EXPECT.txt46 B
- 参考文档tests/fixtures/check-premium/bad-catalogs-missing-once-due/mutation.json61 B
- 参考文档tests/fixtures/check-premium/bad-consumable/EXPECT.txt36 B
- 参考文档tests/fixtures/check-premium/bad-consumable/mutation.json190 B
- 参考文档tests/fixtures/check-premium/bad-debug-premium-in-app/EXPECT.txt67 B
- 参考文档tests/fixtures/check-premium/bad-debug-premium-in-app/mutation.json419 B
- 参考文档tests/fixtures/check-premium/bad-family-sharing/EXPECT.txt17 B
- 参考文档tests/fixtures/check-premium/bad-family-sharing/mutation.json177 B
- 参考文档tests/fixtures/check-premium/bad-flow-in-flows/EXPECT.txt65 B
- 参考文档tests/fixtures/check-premium/bad-flow-in-flows/mutation.json169 B
- 参考文档tests/fixtures/check-premium/bad-generated-storekit-family-sharing/EXPECT.txt77 B
- 参考文档tests/fixtures/check-premium/bad-generated-storekit-family-sharing/mutation.json819 B
- 参考文档tests/fixtures/check-premium/bad-get-task-allow/EXPECT.txt44 B
- 参考文档tests/fixtures/check-premium/bad-get-task-allow/mutation.json230 B
- 参考文档tests/fixtures/check-premium/bad-harness-maestro-env/EXPECT.txt99 B
- 参考文档tests/fixtures/check-premium/bad-harness-maestro-env/mutation.json473 B
- 参考文档tests/fixtures/check-premium/bad-harness-maestro-udid/EXPECT.txt91 B
- 参考文档tests/fixtures/check-premium/bad-harness-maestro-udid/mutation.json242 B
- 参考文档tests/fixtures/check-premium/bad-harness-relaunch-wait/EXPECT.txt303 B
- 参考文档tests/fixtures/check-premium/bad-harness-relaunch-wait/mutation.json467 B
- 参考文档tests/fixtures/check-premium/bad-harness-shared-port/EXPECT.txt106 B
- 参考文档tests/fixtures/check-premium/bad-harness-shared-port/mutation.json325 B
- 参考文档tests/fixtures/check-premium/bad-import-outside/EXPECT.txt71 B
- 参考文档tests/fixtures/check-premium/bad-import-outside/mutation.json251 B
- 参考文档tests/fixtures/check-premium/bad-kitapi/EXPECT.txt38 B
- 参考文档tests/fixtures/check-premium/bad-kitapi/mutation.json202 B
- 参考文档tests/fixtures/check-premium/bad-missing-notice/EXPECT.txt34 B
- 参考文档tests/fixtures/check-premium/bad-missing-notice/mutation.json80 B
- 参考文档tests/fixtures/check-premium/bad-no-plugin/EXPECT.txt15 B
- 参考文档tests/fixtures/check-premium/bad-no-plugin/mutation.json148 B
- 参考文档tests/fixtures/check-premium/bad-not-yet-due/EXPECT.txt300 B
- 参考文档tests/fixtures/check-premium/bad-not-yet-due/mutation.json307 B
- 参考文档tests/fixtures/check-premium/bad-onside-env/EXPECT.txt33 B
- 参考文档tests/fixtures/check-premium/bad-onside-env/mutation.json197 B
- 参考文档tests/fixtures/check-premium/bad-plugin-options/EXPECT.txt73 B
- 参考文档tests/fixtures/check-premium/bad-plugin-options/mutation.json181 B
- 参考文档tests/fixtures/check-premium/bad-price-target/EXPECT.txt153 B
- 参考文档tests/fixtures/check-premium/bad-price-target/mutation.json431 B
- 参考文档tests/fixtures/check-premium/bad-product-id/EXPECT.txt43 B
- 参考文档tests/fixtures/check-premium/bad-product-id/mutation.json197 B
- 参考文档tests/fixtures/check-premium/bad-sdk-caret/EXPECT.txt38 B
- 参考文档tests/fixtures/check-premium/bad-sdk-caret/mutation.json169 B
- 参考文档tests/fixtures/check-premium/bad-sktestsession-in-app/EXPECT.txt60 B
- 参考文档tests/fixtures/check-premium/bad-sktestsession-in-app/mutation.json203 B
- 参考文档tests/fixtures/check-premium/bad-state-testid/EXPECT.txt38 B
- 参考文档tests/fixtures/check-premium/bad-state-testid/mutation.json180 B
- 参考文档tests/fixtures/check-premium/bad-store-not-gated/EXPECT.txt92 B
- 参考文档tests/fixtures/check-premium/bad-store-not-gated/mutation.json284 B
- 参考文档tests/fixtures/check-premium/bad-storekit-consumable/EXPECT.txt18 B
- 参考文档tests/fixtures/check-premium/bad-storekit-consumable/mutation.json196 B
- 参考文档tests/fixtures/check-premium/bad-storekit-display-price/EXPECT.txt92 B
- 参考文档tests/fixtures/check-premium/bad-storekit-display-price/mutation.json199 B
- 参考文档tests/fixtures/check-premium/bad-storekit-file-in-app/EXPECT.txt52 B
- 参考文档tests/fixtures/check-premium/bad-storekit-file-in-app/mutation.json124 B
- 参考文档tests/fixtures/check-premium/bad-typed-price/EXPECT.txt73 B
- 参考文档tests/fixtures/check-premium/bad-typed-price/mutation.json249 B
- 参考文档tests/fixtures/check-premium/good/CASE.txt59 B
03 SKILL.md 正文
Premium purchase
Makes each game sell its single non-consumable "Premium" honestly and with no server: StoreKit 2 verifies on the phone, Premium is saved before a transaction is finished, only explicit evidence ever revokes it, every S12 state is reachable and tested, and two scripts prove it on the real code.
Rules that must hold
- Save Premium before finishing:
persistPremium({ isPremium: true })(synchronous) -> dispatchpremium-granted->finish(tx). Never finish first, never auto-finish, and never finish whenpersistPremiumreturnsfalse(the change did not reach the disk:save.updateansweredfalsefor a refused write, a rejected document, a pending quarantine copy or a newer app's read-only save). On AndroidfinishTransaction({ purchase, isConsumable: false })is the acknowledge, called only from the port'sfinish()and never withisConsumable: true(that consumes Premium); an unacknowledged purchase is never delivered again and Google refunds it after 3 days, sorecheckPremiumandrestorePremiumfinish every owned transaction markedisUnfinished(the adapter'sisAcknowledgedAndroid === false) oncepersistPremiumreturnedtrue. Why: StoreKit re-delivers unfinished transactions; a finished-but-unsaved purchase is lost for good, and an unacknowledged Android one is refunded. - Revoke only on explicit evidence: a verified Premium transaction with a revocation date and no newer valid one. Absence, errors and offline keep the cached Premium; a revocation is saved with its
revokedAtMs. On Android a refund leaves no dated proof on the phone (it only vanishes from the query), so absence never revokes there (owner default a). Why: a fresh purchase can be briefly missing from StoreKit, and the save guard ignores a revoke without a date. - Listen first:
startPremiumsubscribes to purchase events beforeinitConnection, then loads the price, then re-checks silently (Transaction.all,onlyIncludeActiveItemsIOS: false). Why: StoreKit replays unfinished and approved Ask-to-Buy transactions right after connecting, and onlyTransaction.allshows refunds. - Every handler is idempotent; a failure counts only while a purchase is in flight. Why: iOS reports one failure twice (rejection and listener) and re-delivers owned purchases on relaunch.
- Restore distinguishes "couldn't restore" from "nothing to restore": a failed sync (cancelled Apple Account prompt, offline) is always
restore-failed;restore-emptyonly after a successful sync with no evidence. On AndroidrestorePurchasesis the Play query itself: billing unavailable, no Play Store or offline isrestore-failed, never a query error swallowed into[]. Why: telling a paying player "no purchase found" because the network failed is the worst answer. expo-iapis exactly 5.8.0, imported only byexpo-iap-purchase-adapter.ts, and its plugin entry is the bare string'expo-iap'. On Android it runs Play Billing 9.1.0 and needs['expo-build-properties', { android: { kotlinVersion: '2.1.20' } }]in the same plugin list; the adapter keepsstate: purchase.purchaseState, so a pending Play purchase is never granted or acknowledged. NeverkitApi,KitApiError,verifyPurchaseWithProvider,verifyPurchase,useIAP,andDangerouslyFinishTransactionAutomatically, any plugin option,ios.onside.enabledorEXPO_IAP_ONSIDE. Why: they add a server (IAPKit) or the OnsideKit pod (spec N2/N3), or hide the finishing logic.- One product per game:
<bundleId>.premium(io.applander.<game id without hyphens>.premium, owner decision O4), NON_CONSUMABLE, the EUR 1.99 App Store price point (O2),familySharable: false(O3: Family Sharing stays off, never turned on). Every StoreKit configuration (the template and each generated*.storekit) and the ASC payload sayfalse;create-premium-iap.tstargets exactly 1.99. On Google Play the same id is a one-time (managed) product at a EUR 1.99 default price with Google's local prices (owner step GP6, after the first internal upload). Why: spec N7; the owner decided the price and Family Sharing on 2026-09-30; product IDs can never be reused, and Family Sharing cannot be turned off once on. - An empty product list, a failed connection or being offline is "store unavailable", never an error dialog; the port is
withConnectivity(createExpoIapPurchaseAdapter(), connectivity.isOnline), and a connectivity change reloads the store whenshouldReloadStoresays so. Apple's error texts are never shown. Why: StoreKit returns[]instead of throwing; S12 has its own catalog messages; and the debug "Simulate offline" switch (Tier 2 flow 07) only reaches the store through ConnectivityPort. - Show the price the store reports, in the player's digits (
formatStorePrice, fallbackdisplayPrice). Never type a price into code or catalogs; catalogs use{priceText}. The EUR 1.99 point is what App Store Connect sells in Germany; players elsewhere see their own territory's price. Why: spec S12 and Apple's per-territory price points. - Every S12 state is designed and tested: reducer tests for each (Tier 1), the StoreKit harness before each release and after every expo-iap upgrade (Tier 2), and the owner's TestFlight run per game (Tier 3). Why: spec S12/15.5; the real purchase sheet only exists on TestFlight.
- All S12 text comes from the Shell catalogs in en, de, fa and ckb. Why: App Store Connect has no Persian or Sorani, and its texts are never shown in the app.
- The StoreKit harness exists only in throwaway harness builds;
get-task-allow,*.storekitand*.xctestnever ship. Why:get-task-allowin a store build is a security hole and a rejection. - Premium changes only through the reducer, after the save: purchases, restores and re-checks via the service; the test build's debug switch (
premium=0|1) viadebug-premium-set, only from test-only code. Why: one path keeps ads, the save and S12 in step, and a stray unlock would give Premium away. - Premium is never pushed with pop-ups: Home button (hidden once owned), Settings row, at most one Result-screen line per day (
premiumNudgePriceagainst the save'supsell.lastShownOn). Why: spec S12 rules. - The owner does the App Store Connect human steps (agreements, the app record with its fixed id, attaching Premium to the first version, the TestFlight test); the price (EUR 1.99) and Family Sharing (off) are decided, so there is no price or Family Sharing question. Ask in plain words and wait; never submit anything yourself, and stop only if Apple no longer offers EUR 1.99.
Workflow
- Read references/store-and-adapter.md. Install
npm install -E expo-iap@5.8.0in every app (Shell step 5, with the services). The bare string'expo-iap'(no options) is a line of the Shell's one plugin list (Shell step 8),shellPluginsinpackages/shell/src/config/shell-plugins.ts(architecture-and-boundaries ships it); never add a second list. Setpremium.productIdto<bundleId>.premiumin eachgame.config.ts. - At Shell step 5 copy the templates at the same relative paths:
templates/packages/shell/src/services/purchase/,templates/packages/shell/src/stores/premium/,templates/__mocks__/expo-iap.ts(each test lands with its file; they need only step 5's prerequisites: zustand,testing/create-test-save.ts,testing/flush-microtasks.tsandapp/stores-context.tsx). Keep existingpackages/shell/src/app/stores-context.tsxand the save service; the store needs both. - Read references/premium-states.md. The composition root is game-host-integration's template set (
packages/shell/src/app/, Shell step 7): it builds the port where the adapter is created (withConnectivity(createExpoIapPurchaseAdapter(), isOnline)indevice-adapters.ts), the dependencies once (create-premium-deps.ts), callsstartPremium(never awaited) and right after itconnectPremiumReloads(connectivity, stores, premiumDeps)(connect-premium-reloads.ts:loadStorewhenshouldReloadStore(flow, isOnline)on a connectivity change,recheckPremiumwhen the app comes back to the foreground online).startPremiumruns before the first network state arrives, so without the reloads Settings and S12 never show a price after an offline start. WirebuyPremiumandrestorePremiumon the S12 buttons, and render S12 frompremiumViewandpremiumNoticewith the test IDs and copy keys in its tables. examples/wire-premium.md shows each call in place. The S12 layout comes from its Toybox design (hand off totoybox-screens), and every state screen must then match its design screenshot (toybox-visual-parity). - Make sure the four Shell catalogs hold every Premium key from the copy deck (the i18n work owns the texts), with
{priceText}in the price keys. - Run Tier 1:
npx jest packages/shell/src/stores/premium packages/shell/src/services/purchase packages/shell/src/app/connect-premium-reloads.test.ts --ci --selectProjects unit --coverage --collectCoverageFrom='packages/shell/src/{stores/premium,services/purchase}/**/*.ts' --coverageThreshold='{}'(paths first; onlynpm run test:coveragejudges the thresholds). - Run
node ${CLAUDE_SKILL_DIR}/scripts/check-premium.mjs .andnode ${CLAUDE_SKILL_DIR}/scripts/check-premium-behaviour.mjs .. Fix everyFAILline (file, rule, fix) and rerun until both printRESULT: PASS. Early in the Shell build order two rules are not due yet and printSKIPlines instead (a pass):plugin-entryuntilpackages/shell/src/config/shell-plugins.tsexists (Shell step 8) andcatalog-keysuntil the catalog linterpackages/tooling/src/i18n/verify-catalogs.tsexists (Shell step 6); once the file exists the rule is strict (missing Shell catalogs then fail).price-targetandfamily-sharingread the ASC tooling and every StoreKit configuration once they are in the repo. - Before a release (Shell step 11 for the pilot, game step 14 for a new game) and after an expo-iap upgrade: read references/testing-tiers.md, copy
templates/packages/tooling/src/storekit/,templates/packages/tooling/src/e2e/maestro-args.ts(+ test, if the repo lacks e2e-maestro's copy) andtemplates/packages/shell/e2e/storekit/, and make surepackages/shell/e2e/subflows/debug-setup.yamlis the Shell's one copy (e2e-maestro installs the same bytes;templates/packages/shell/e2e/subflows/debug-setup.yamlis that shared file: it waits for the app's first screen before it opens the debug link, and taps only Apple's "Open in ...?" alert, never a bare 'Open'). Run the Tier 2 harness on a throwaway simulator of your own (e07-<purpose>; the harness gives Maestro the global--device <udid>and a driver port of its own for each flow, and Metro its own port) in the release-day order of testing-tiers.md: the E2E evidence run first, then the harness, thenxcrun simctl delete <udid>, thennpx expo prebuild --platform ios --cleanbefore any Release or store build. - For the store step (Shell step 11 for the pilot, game step 14 for a new game, with the harness) read references/app-store-connect.md: ask the owner for the human steps, copy
templates/packages/tooling/src/asc/(keep existing ASC client files) andtemplates/packages/tooling/src/clock/system-clock.tsif missing, and runcreate-premium-iap.ts <bundleId>: it sets exactly the EUR 1.99 price point with Family Sharing off, and stops (then ask the owner) only if Apple no longer offers EUR 1.99. - Ask the owner for the Tier 3 TestFlight run (buy, cancel, reinstall + restore) and report the result in plain words.
- On Android (once the game's iOS version has shipped): read references/google-play-billing.md. Copy
expo-iap-purchase-adapter.android.test.tswith the other purchase templates andpremium-flow.android.test.tswith the premium store templates (both run in theunit-androidJest project), make sureshellPluginsholds['expo-build-properties', { android: { kotlinVersion: '2.1.20' } }]next to'expo-iap', rerun both checkers, and ask the owner for GP6 (the Play product, after the first internal upload) and the Android Tier 3 run as a license tester (buy, cancel, pending with the slow test card, reinstall from Play + restore). The StoreKit harness has no Android counterpart; an emulator oradb installbuild correctly shows "store unavailable".
Definition of done
- [ ]
expo-iapis exactly 5.8.0, imported only by the adapter; the plugin entry is the bare string; no banned API, option or Onside switch anywhere. - [ ] Every template file is in place and Tier 1 passes (reducer, notice, nudge, evidence, service, connectivity gate, price, ASC payloads).
- [ ] A purchase saves Premium, updates ads at once, then finishes; pending shows "Waiting for approval" and an approval later turns Premium on without a tap.
- [ ] Restore shows
restore-failedafter a failed sync andrestore-emptyonly after a successful one; refunds revoke only with a revocation date. - [ ] Offline (and with the debug offline switch) S12 shows "store unavailable" without asking StoreKit: the port is wrapped in
withConnectivitywhere the adapter is created; after an offline start the price loads once the network comes (connectPremiumReloadsafterstartPremium). - [ ] Prices come from the store in the player's digits; S12 texts exist in all four catalogs; every S12 state has its test ID.
- [ ] Every S12 state screen has passed the Toybox visual-parity check against its design screenshot.
- [ ] Before a release: the seven Tier 2 scenarios pass on a throwaway simulator in the release-day order (E2E, harness, delete the simulator, clean prebuild); the store IPA has no
*.storekit,*.xctestorget-task-allow; the owner's Tier 3 run passed for this game and version. - [ ] Premium is
<bundleId>.premiumat the EUR 1.99 price point with Family Sharing off in every StoreKit configuration and ASC payload (price-target,family-sharing). - [ ] The product exists in App Store Connect (en-US/de-DE texts, EUR 1.99, Family Sharing off, availability, review screenshot) and is attached to the first version.
- [ ] Android: Premium is saved before the purchase is acknowledged and never consumed, a pending Play purchase is neither granted nor acknowledged, the launch re-check acknowledges a saved but unacknowledged purchase, absence never revokes (owner default a), Kotlin 2.1.20 is set through
expo-build-properties, andexpo-iap-purchase-adapter.android.test.tsandpremium-flow.android.test.tspass inunit-android. - [ ]
node ${CLAUDE_SKILL_DIR}/scripts/check-premium.mjs .andnode ${CLAUDE_SKILL_DIR}/scripts/check-premium-behaviour.mjs .both printRESULT: PASS
Anti-patterns
- Finishing the transaction first "so StoreKit stops re-sending it". Re-delivery is the safety net; save, grant, then finish.
- Turning Premium off because the re-check found nothing. Absence is not evidence; only a revocation date is.
- Showing "No purchase to restore" when
restorePurchasesthrew. That isrestore-failed. useIAPorverifyPurchase"for simplicity". They hide finishing or call a server; the adapter template is the whole integration.- Giving the plugin
{ iapkitApiKey }or any options object. Even an empty object fails the audit; use'expo-iap'. - Typing "€1.99" in a catalog, a test fixture of the screen, or a component. Use
{priceText}andformatStorePrice. - Reading
getAvailablePurchases()with the default active-only list. Refunds disappear from it; passonlyIncludeActiveItemsIOS: false. launchApp: { clearState: true }in a Tier 2 flow. Uninstalling clears the test transactions.- A short wait in a relaunch flow. StoreKit answers the launch re-check only after its transaction sync (28 s once after a refund); the refund and approval relaunch flows wait 60 s for their state (
harness-relaunch-wait). Never shorten it to make a run faster. - Handing the raw expo-iap adapter to the service. Without
withConnectivitythe store ignores ConnectivityPort: offline S12 waits on StoreKit and Tier 2 flow 07 (offline=1) shows a price instead of "store unavailable". - Putting a StoreKit flow under
e2e/flows/. The normal E2E run would run it against an unarmed build. - Pushing Premium with a pop-up after a loss or on launch. Only the three quiet entry points.
- Editing a Jest test or a fixture until the checker passes. The behaviour check runs the real modules; fix the module.
Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| references/store-and-adapter.md | Premium rules, the three StoreKit facts, versions, bans, PurchasePort, adapter facts, error-code table, re-verify | Workflow step 1, and before touching the adapter |
| references/premium-states.md | Reducer and actions, the S12 state table (views, test IDs, copy keys), restore toasts, service order, wiring, edge cases, price display | Workflow step 3, and on any state change |
| references/testing-tiers.md | Tier 1 tests, the verified StoreKit harness, its files, scenarios, commands and gotchas, Tier 3 steps | Workflow steps 5 and 7 |
| references/app-store-connect.md | Product rules, the EUR 1.99 price (O2), Family Sharing off (O3), the ASC API script, remaining steps, human steps | Workflow step 8 |
| references/google-play-billing.md | Android: Play Billing 9.1.0 and Kotlin 2.1.20, acknowledge after the save, the 3-day refund of unacknowledged purchases, pending, restore, refunds (absence never revokes, owner default a), the GP6 product at EUR 1.99, license testers, Play-installed builds | Workflow step 10, and before touching Android purchase code |
| examples/wire-premium.md | The complete wiring for the pilot game | Workflow step 3, as the model |
templates/packages/shell/src/services/purchase/ |
Port, expo-iap adapter, fake, connectivity gate withConnectivity (+ test), service (+ test; the test is synced from the library, the same file as unit-and-component-tests' example: do not edit here), store flow with shouldReloadStore, the Android adapter test expo-iap-purchase-adapter.android.test.ts (the unit-android project), price formatter (+ test) |
Workflow step 2 |
templates/packages/shell/src/stores/premium/ |
State, transitions, reducer (+ test), view, notice (+ test), Result-screen nudge (+ test), evidence (+ test), Zustand store, and premium-flow.android.test.ts (the unit-android project: listening before initConnection, the launch re-check acknowledging a saved but unacknowledged purchase, offline keeping the cached Premium) |
Workflow step 2 |
templates/__mocks__/expo-iap.ts |
Jest root mock (real ErrorCode, no banned APIs) |
Workflow step 2 |
templates/packages/tooling/src/storekit/ |
Premium.storekit.template (EUR 1.99, Family Sharing off), storekit-harness.entitlements, ArmTests.swift, add-harness.rb, storekit-harness.ts (explicit --device, its own driver port for each flow, its own Metro port), metro-session.ts with its test (Metro's output in build/logs/storekit-metro.log, Metro stopped however the run ends), busy-note.ts with its test (the log line that names a busy Mac) |
Workflow step 7 |
templates/packages/tooling/src/e2e/ |
maestro-args.ts: the shared Maestro target helper (maestroGlobalArgs, freeDriverPort, driverPortFor) with its test; e2e-maestro's file, synced from the library (do not edit here) |
Workflow step 7, with the harness |
templates/packages/shell/e2e/storekit/ |
The seven Tier 2 Maestro flows (the two relaunch flows wait 60 s for StoreKit's launch re-check) | Workflow step 7 |
templates/packages/shell/e2e/subflows/debug-setup.yaml |
The debug deep-link subflow the flows call (the Shell's one shared copy, synced from the library: it waits for the first screen before the link and taps only Apple's "Open in ...?" alert; do not edit here) | Workflow step 7 |
templates/packages/tooling/src/asc/ |
ASC payloads (+ test), create-premium-iap.ts (the CLI with TARGET_EUR) over premium-iap-setup.ts (+ test on a fake App Store Connect: the bundle id checked with ios-simulator-build's isAppId before the key is read, exit 2 with the usage; the app found by its exact bundle id), find-app.ts (+ test; synced from the library, identical to ios-release-testflight's copy), JWT, credentials and client |
Workflow step 8 |
templates/packages/tooling/src/clock/system-clock.ts |
The tooling wall clock with nowEpochSeconds and todayIso |
Workflow step 8, if missing or lacking nowEpochSeconds |
assets/premium-facts.json |
Pinned version, adapter allowlist, banned APIs and config words, catalog keys, S12 test IDs, views, required files, Family Sharing decision | Read by the checkers; update on an upgrade or an owner decision |
scripts/check-premium.mjs |
Static checks: files, pin, imports, banned APIs, plugin options, adapter facts, the adapter wrapped in withConnectivity where it is created, the debug Premium action only in test-only code, product IDs, typed prices (the parity harness's fixture store excepted), catalog keys, S12 test IDs, harness isolation (the repo scan skips skills/, .claude/ and generated folders), StoreKit config, Family Sharing, the EUR 1.99 target and every StoreKit displayPrice (price-target) and Family Sharing off in the StoreKit template, every generated apps/<id>/ios/*.storekit and the ASC payload (family-sharing), the StoreKit harness's Maestro calls through maestroGlobalArgs, never --udid, each flow on its own driver port (harness-maestro), and the 60 s wait of the two relaunch flows (harness-relaunch-wait); on Android the acknowledge only in finish() and never consumed (android-acknowledge), state: purchase.purchaseState (android-pending), Kotlin through expo-build-properties (android-kotlin) |
Workflow step 6 |
scripts/check-premium-behaviour.mjs |
Runs the repo's Premium modules (type stripping) against the S12 table, evidence, service order, restore, revocation, offline gate, Result-screen nudge and price rules, the expo-iap adapter against a scripted expo-iap (error codes, restore, Transaction.all mapping, call shapes), the composition root's two store reloads (store-reloads), and Google Play Billing through the real adapter with Play's purchase shape (android-pending, android-restore, android-refund) |
Workflow step 6 |
scripts/lib/repo-scan.mjs |
File listing and import parsing helpers | Read only to change a checker |
scripts/lib/load-ts.mjs |
Imports repo TypeScript modules with the @e07/* aliases and the module stand-ins |
Read only to change a checker |
scripts/lib/stubs/expo-iap.mjs |
Scripted Node stand-in for expo-iap 5.8.0 used by the behaviour check (update ErrorCode on an upgrade) |
Read only to change a checker |
scripts/lib/assemble-fixtures.mjs |
Builds each self-test case from templates + base repo + mutation.json |
Read only to add a self-test case |
scripts/selftest.mjs |
Proves both checkers on the clean repo and 67 planted problems (the Android acknowledge, pending, Kotlin, restore and refund cases included) | After changing a checker, a template or a fixture |
scripts/check-lib.mjs |
Shared script helper, synced from the library (do not edit here) | Never by hand |
assets/shared.json |
Declares the shared files copied into this skill | When adding a shared file |
tests/fixtures/ |
base-repo/ plus one mutation.json and EXPECT.txt per case, per checker |
When adding a rule |
Related skills
admob-ads- Premium turns ads off and perks free; it readsisPremium.state-stores- the store pattern andcreateShellStores, which creates the Premium store.save-persistence-and-migrations- the save'spremiumsection and its revoke guard.toybox-screens- the S12 layout that renders the states and toasts.toybox-visual-parity- proves each S12 state screen matches its design screenshot.i18n-strings-and-catalogs- the Premium texts in four languages.privacy-and-network-audit- the network audit that fails on IAPKit or Onside.ios-release-testflight- signing, the store-artifact gate and the TestFlight human steps.e2e-maestro- Maestro installation and the debug deep link.
04 同仓库更多 Skills
unit-and-component-tests
Sets up Jest and writes Pocket Arcade test code - three projects, RNTL 14 async via renderWithShell, root SDK mocks, fakes, fast-check, coverage, Stryker. Use when a test needs the Shell wrapper, a mock or a fake, or Jest fails. Not for the red-green loop (tdd-workflow) or goldens (golden-tests).
typescript-and-lint-rules
Enforces Pocket Arcade's strict TypeScript 6 tsconfigs, ESLint 9 flat config, Prettier and size limits. Use when writing or splitting .ts/.tsx or fixing a tsc or lint error. Not for gate wiring (quality-gates), names (naming-conventions) or import zones (architecture-and-boundaries).
demo-skill
Builds a demo. Use when testing the pitfall checker.
demo-skill
Builds a demo. Use when testing the pitfall checker.
troubleshooting-playbook
Diagnoses and fixes Pocket Arcade failures with a catalogue of 300+ known ones (symptom, cause, fix). Use when something errors, crashes, hangs or renders wrong, or an error or log needs explaining. Not for gate setup (quality-gates) or running builds (ios-simulator-build, ios-release-testflight).
toybox-visual-parity
Proves a built Pocket Arcade screen matches its Toybox design screenshot (light/dark, en/fa) and drives the fixes: capture, element gates, sheets, sign-off. Use when any screen is built or changed, or for visual parity, design match, screenshot compare. Not for building screens (toybox-screens).