{"id":49688130,"url":"https://github.com/perryts/storekit","last_synced_at":"2026-05-07T11:01:09.372Z","repository":{"id":356260866,"uuid":"1188560094","full_name":"PerryTS/storekit","owner":"PerryTS","description":"StoreKit 2 in-app purchase bindings for Perry apps (iOS/macOS)","archived":false,"fork":false,"pushed_at":"2026-05-07T09:06:56.000Z","size":24,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-07T10:34:43.350Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PerryTS.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-22T08:56:50.000Z","updated_at":"2026-05-07T09:25:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/PerryTS/storekit","commit_stats":null,"previous_names":["perryts/storekit"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/PerryTS/storekit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerryTS%2Fstorekit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerryTS%2Fstorekit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerryTS%2Fstorekit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerryTS%2Fstorekit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PerryTS","download_url":"https://codeload.github.com/PerryTS/storekit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerryTS%2Fstorekit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32734391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T02:14:30.463Z","status":"ssl_error","status_checked_at":"2026-05-07T02:14:29.405Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-07T11:00:37.704Z","updated_at":"2026-05-07T11:01:09.359Z","avatar_url":"https://github.com/PerryTS.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @perryts/storekit\n\nStoreKit 2 in-app purchase bindings for [Perry](https://github.com/PerryTS/perry) — closes [PerryTS/perry#537](https://github.com/PerryTS/perry/issues/537).\n\n## Platforms\n\n| Target          | Implementation                                                          |\n| --------------- | ----------------------------------------------------------------------- |\n| iOS 16+         | Native — Swift bridge over StoreKit 2 (`Product.products(for:)`, etc.). |\n| macOS 13+       | Native — same Swift bridge.                                             |\n| Linux / Windows | Stub — every call resolves with a `\"not available\"` JSON payload.       |\n| Android         | Stub. (Google Play Billing is a separate binding — see issue #537.)     |\n\n## Installation\n\n```bash\nnpm install @perryts/storekit\n```\n\nThe package targets perry-ffi ABI v0.5 (`perry.nativeLibrary.abiVersion: \"0.5\"` in `package.json`). Perry validates compatibility at build time.\n\n## Quick start\n\n```typescript\nimport {\n  js_storekit_start_listener,\n  js_storekit_load_products,\n  js_storekit_purchase,\n  js_storekit_has_subscription,\n  js_storekit_get_jws,\n  js_storekit_restore,\n} from \"@perryts/storekit\";\n\n// Boot the Transaction.updates listener once at launch — handles\n// Ask-to-Buy approvals, family-shared entitlements, auto-renew, etc.\njs_storekit_start_listener();\n\n// Load products you have configured in App Store Connect.\nconst productsJson = await js_storekit_load_products(\n  \"com.example.pro_monthly,com.example.pro_annual\",\n);\nconst products = JSON.parse(productsJson);\n\n// Drive the purchase sheet. The product must have been loaded first —\n// StoreKit 2 needs the in-memory `Product` value to call `purchase()`.\nconst purchaseJson = await js_storekit_purchase(\"com.example.pro_monthly\");\nconst purchase = JSON.parse(purchaseJson);\nif (purchase.success) {\n  // purchase.jws → server-side receipt validation\n  // purchase.transactionId, purchase.purchaseDate → audit log\n}\n\n// Check entitlements at any time.\nconst subJson = await js_storekit_has_subscription();\nconst { hasSubscription } = JSON.parse(subJson);\n```\n\n## Typed wrapper (recommended)\n\nThe native FFI returns JSON strings so the cross-language contract stays simple. In your app, wrap the calls with the types this package re-exports:\n\n```typescript\nimport {\n  js_storekit_load_products,\n  js_storekit_purchase,\n  js_storekit_has_subscription,\n  js_storekit_get_jws,\n  js_storekit_restore,\n  type Product,\n  type PurchaseResult,\n  type HasSubscriptionResult,\n  type JwsResult,\n  type RestoreResult,\n} from \"@perryts/storekit\";\n\nexport async function loadProducts(ids: string[]): Promise\u003cProduct[]\u003e {\n  const json = await js_storekit_load_products(ids.join(\",\"));\n  const parsed = JSON.parse(json);\n  if (parsed \u0026\u0026 typeof parsed === \"object\" \u0026\u0026 \"error\" in parsed) {\n    throw new Error(parsed.error as string);\n  }\n  return parsed as Product[];\n}\n\nexport async function purchase(productId: string): Promise\u003cPurchaseResult\u003e {\n  const json = await js_storekit_purchase(productId);\n  return JSON.parse(json) as PurchaseResult;\n}\n\nexport async function hasSubscription(): Promise\u003cboolean\u003e {\n  const json = await js_storekit_has_subscription();\n  return (JSON.parse(json) as HasSubscriptionResult).hasSubscription;\n}\n\nexport async function getJWS(): Promise\u003cstring | null\u003e {\n  const json = await js_storekit_get_jws();\n  return (JSON.parse(json) as JwsResult).jws;\n}\n\nexport async function restorePurchases(): Promise\u003cRestoreResult\u003e {\n  const json = await js_storekit_restore();\n  return JSON.parse(json) as RestoreResult;\n}\n```\n\nThe `Product` and `PurchaseResult` shapes match the sketch in [issue #537](https://github.com/PerryTS/perry/issues/537), with one practical addition: `PurchaseResult.jws` carries the App Store-issued JWS, which is what Apple's [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi) expects for server-side validation. (The legacy base64 `receipt` is no longer the recommended StoreKit 2 path.)\n\n## API reference\n\n### `js_storekit_start_listener(): void`\n\nStart `Transaction.updates` in a detached `Task`. Verified transactions are automatically `finish()`-ed. Call exactly once at app launch — calling again cancels the previous listener.\n\n### `js_storekit_load_products(commaSeparatedIds: string): Promise\u003cstring\u003e`\n\nResolves with a JSON array of [`Product`](./src/index.ts) objects. On failure: `{\"error\": \"...\"}`.\n\nLoaded products are cached in a Swift `actor` so `js_storekit_purchase` can look them up by ID.\n\n### `js_storekit_purchase(productId: string): Promise\u003cstring\u003e`\n\nResolves with a JSON [`PurchaseResult`](./src/index.ts). Possible shapes:\n\n```json\n{ \"success\": true,  \"jws\": \"eyJ…\", \"productId\": \"…\", \"transactionId\": \"…\", \"purchaseDate\": \"2026-05-07T10:23:11.123Z\", \"cancelled\": false }\n{ \"success\": false, \"cancelled\": true }\n{ \"success\": false, \"pending\": true }\n{ \"success\": false, \"error\": \"…\" }\n```\n\n### `js_storekit_has_subscription(): Promise\u003cstring\u003e`\n\nResolves with `{\"hasSubscription\": boolean}`. True iff at least one of `Transaction.currentEntitlements` is verified and has no `revocationDate`.\n\n### `js_storekit_get_jws(): Promise\u003cstring\u003e`\n\nResolves with `{\"jws\": \"…\"}` (most recent verified entitlement) or `{\"jws\": null}` (no active entitlement). Hand the JWS to your server, validate against Apple, then trust it.\n\n### `js_storekit_restore(): Promise\u003cstring\u003e`\n\nCalls `AppStore.sync()`. Resolves with `{\"success\": true}` or `{\"error\": \"…\", \"success\": false}`. Apple recommends only invoking this from a user-tapped \"Restore Purchases\" button.\n\n## How it's wired\n\n```\nTypeScript                Rust (perry-ffi 0.5)         Swift (StoreKit 2)\n-------------------       ----------------------       ----------------------\njs_storekit_purchase  →   #[no_mangle] extern \"C\"  →   @_cdecl bridge fn\n                          fn js_storekit_purchase      runs Task { … }\n                          returns *mut Promise         calls back with JSON\n                          ←─── promise.resolve_string(json) ←──────────\n```\n\n* `crate-ios/` — Apple-platform crate. Depends on `perry-ffi = \"0.5\"` (tracked at `git+https://github.com/PerryTS/perry`). Its `build.rs` compiles `swift/storekit_bridge.swift` to a static lib and links it; `package.json` lists `StoreKit` and `Foundation` so perry's link step adds `-framework`.\n* `crate-stub/` — non-Apple crate. Same exported `js_storekit_*` symbol set, but every call resolves immediately with a `\"not available on this platform\"` payload so calling code can fall back to a Stripe/web flow without `#ifdef`-style platform checks.\n* `package.json :: perry.nativeLibrary` — declares `abiVersion: \"0.5\"`, the FFI symbol list, and per-target `crate` / `lib` / `frameworks`.\n\n## Server-side validation\n\nThis binding does not validate JWS receipts itself — that's plain HTTPS against [Apple's App Store Server API](https://developer.apple.com/documentation/appstoreserverapi/verify_a_transaction). A typical flow:\n\n1. Client: `js_storekit_purchase(\"…\")` → JWS.\n2. Client → your server: `POST /verify-storekit { jws }`.\n3. Server: validate signature, check `transactionId`, mark entitlement.\n4. Server → client: confirmation.\n\nPeriodically poll `js_storekit_has_subscription()` (or react to `Transaction.updates`) to keep the local cache fresh.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fperryts%2Fstorekit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fperryts%2Fstorekit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fperryts%2Fstorekit/lists"}