{"_id":"@appaflytech/wappa-inapp-purchase","name":"@appaflytech/wappa-inapp-purchase","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@appaflytech/wappa-inapp-purchase","version":"0.0.1","description":"Drop-in App Store / Google Play in-app purchases for Expo / React Native apps, powered by the Wappa panel. Wraps expo-iap with full offer, promo-code, upgrade and entitlement support, verifies receipts server-side, and records purchases in your Wappa admi","keywords":["expo","react-native","in-app-purchase","iap","subscriptions","expo-iap","app-store","google-play","wappa","offers","promo-codes","subscription-offers"],"license":"MIT","author":{"name":"Wappa"},"publishConfig":{"access":"public"},"main":"dist/index.js","module":"dist/index.js","react-native":"dist/index.js","types":"dist/index.d.ts","sideEffects":false,"scripts":{"clean":"rm -rf dist","build":"tsc -p tsconfig.json","typecheck":"tsc --noEmit -p tsconfig.json","prepare":"npm run build","prepublishOnly":"npm run clean && npm run build"},"peerDependencies":{"expo-iap":">=5.0.0","react":">=17.0.0","react-native":">=0.68.0"},"peerDependenciesMeta":{"react":{"optional":true},"expo-constants":{"optional":true}},"devDependencies":{"typescript":"^5.4.5"},"engines":{"node":">=18"},"_id":"@appaflytech/wappa-inapp-purchase@0.0.1","gitHead":"62858c5432037b2dc6eec1333c7e8b83f8a79d5b","_nodeVersion":"24.6.0","_npmVersion":"11.5.1","dist":{"integrity":"sha512-Bc/xir41sYggSAjU7zV+hXTk7v2A8fZFiTdbYxeBrzyMo9yTCHPylEOmK8iBCS7CpT+QIF6WCzy/I1ClFpmrCQ==","shasum":"4fe1f7ad8ecc309c47a6e051936cf9a2cc4f2b03","tarball":"https://registry.npmjs.org/@appaflytech/wappa-inapp-purchase/-/wappa-inapp-purchase-0.0.1.tgz","fileCount":35,"unpackedSize":102147,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBmsgizKK8RSd+eyjvsjCV72d8vmWenUQgMKo80iAT1rAiAThuQ5HTF7z7fgG02U25mIlUUCQQ3SiCli3lLQNgI2jg=="}]},"_npmUser":{"name":"emrahyurttutan","email":"emrah@yurttutan.net"},"directories":{},"maintainers":[{"name":"emrahyurttutan","email":"emrah@yurttutan.net"},{"name":"appa-tech","email":"appaflytech@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/wappa-inapp-purchase_0.0.1_1786351400617_0.8546266791080086"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-10T08:43:20.465Z","0.0.1":"2026-08-10T08:43:20.746Z","modified":"2026-08-10T08:43:20.923Z"},"maintainers":[{"name":"emrahyurttutan","email":"emrah@yurttutan.net"},{"name":"appa-tech","email":"appaflytech@gmail.com"}],"description":"Drop-in App Store / Google Play in-app purchases for Expo / React Native apps, powered by the Wappa panel. Wraps expo-iap with full offer, promo-code, upgrade and entitlement support, verifies receipts server-side, and records purchases in your Wappa admi","keywords":["expo","react-native","in-app-purchase","iap","subscriptions","expo-iap","app-store","google-play","wappa","offers","promo-codes","subscription-offers"],"author":{"name":"Wappa"},"license":"MIT","readme":"# wappa-inapp-purchase\n\nExpo / React Native uygulamalarına **App Store / Google Play satın almalarını** ekleyen SDK.\n[`expo-iap`](https://hyochan.github.io/expo-iap/) 5.x üzerine kuruludur; satın alma tamamlanınca\nmakbuzu **Wappa backend'ine sunucu-taraflı doğrulatır** ve **Wappa panelinde** kaydeder.\n\nAbonelikler, tüketilebilir ürünler (jeton/can), kalıcı ürünler (reklam kaldırma), **teklifler**\n(intro / promosyonel / win-back / Play base plan), **promosyon kodları**, abonelik\nyükseltme–düşürme ve iade akışı desteklenir.\n\n```\nKullanıcı satın alır  →  expo-iap (App Store / Google Play)\n        →  POST {site}/subscriptions/{language}/iap/verify  (Bearer JWT)\n        →  Wappa backend makbuzu Apple/Google'a SORAR (istemcinin makbuzu otorite değildir)\n        →  bundle/package + ürün + ortam + iade/süre kontrolleri (hepsi fail-closed)\n        →  satın alma kullanıcıya bağlanır (appAccountToken / obfuscatedAccountId)\n        →  productId → Plan.Code eşleşir; abonelik açılır ya da tek seferlik hak verilir\n        →  Wappa admin \"Abonelikler\" + \"IAP Doğrulama Kayıtları\" ekranlarında görünür\n```\n\n---\n\n## Kurulum\n\n```bash\nnpm install @appaflytech/wappa-inapp-purchase\nnpx expo install expo-iap\n```\n\n`app.json` içindeki `plugins` dizisine `expo-iap`'i ekleyin:\n\n```json\n{ \"expo\": { \"plugins\": [\"expo-iap\"] } }\n```\n\n> `expo-iap` (>=5), `react` ve `react-native` **peer dependency**'dir.\n> Satın alma test etmek için **development / standalone build** gerekir (Expo Go desteklemez).\n\n---\n\n## Env değişkenleri\n\n```bash\nEXPO_PUBLIC_WAPPA_SITE_KEY=my-site\n# opsiyonel — varsayılan hosted panele bağlanır\nEXPO_PUBLIC_WAPPA_API=https://wappa-ui-api.appaflytech.com\n```\n\n---\n\n## Panel tarafında yapılması gerekenler\n\n1. **IAP Ayarları** ekranında bir **Organizasyon** seçin. Store ürün id'si ancak bu\n   organizasyondaki planlara eşlenir — seçilmeden hiçbir satın alma kaydedilemez.\n2. Her store ürünü için o organizasyonda bir **Plan** açın; `Kod` alanını store'daki\n   `productId` ile **aynı** yapın (örn. `premium_monthly`).\n3. Plan formunda **Store ürün türü**'nü doğru seçin (Abonelik / Tüketilebilir / Kalıcı).\n   Store'lar bunu doğrulama sırasında söylemez, bu yüzden katalogda beyan edilir.\n4. iOS için **Bundle ID** (zorunlu) ve tercihen **App Store Server API** (.p8) bilgilerini girin.\n5. Android için **Package Name** + **service-account JSON** ve **Pub/Sub audience** girin.\n6. Ekrandaki webhook URL'lerini App Store Connect ve Play RTDN Pub/Sub push aboneliğine yapıştırın.\n\n---\n\n## Kullanım\n\n### A) React hook (önerilen)\n\n```tsx\nimport { useWappaPurchases } from \"@appaflytech/wappa-inapp-purchase\";\nimport { useWappaAuth } from \"@appaflytech/wappa-auth\";\n\nfunction Paywall() {\n  const auth = useWappaAuth();\n\n  const {\n    connected,\n    subscriptions,\n    purchase,\n    restore,\n    presentOfferCodeRedemption,\n    manageSubscriptions,\n  } = useWappaPurchases({\n    subscriptionSkus: [\"premium_monthly\", \"premium_yearly\"],\n    consumableSkus: [\"coins_100\"],\n    getAccessToken: () => auth.getAccessToken(), // doğrulama ucu auth'lu\n    getUserId: () => auth.user?.id,              // makbuzu bu hesaba bağlar\n    onVerified: (sub) => console.log(\"Kaydedildi:\", sub.id),\n    onVerifyError: (e) => {\n      if (e.reason === \"conflict\") alert(\"Bu satın alma başka bir hesaba ait.\");\n    },\n  });\n\n  return (\n    <>\n      {subscriptions.map((s) => (\n        <Button key={s.id} title={s.displayPrice} onPress={() => purchase(s.id)} />\n      ))}\n      <Button title=\"Satın almaları geri yükle\" onPress={restore} />\n      <Button title=\"Promosyon kodu kullan\" onPress={presentOfferCodeRedemption} />\n      <Button title=\"Aboneliği yönet\" onPress={() => manageSubscriptions()} />\n    </>\n  );\n}\n```\n\n### B) Imperative\n\n```ts\nimport { initWappaPurchases } from \"@appaflytech/wappa-inapp-purchase\";\n\nconst iap = await initWappaPurchases({\n  subscriptionSkus: [\"premium_monthly\"],\n  getAccessToken: () => auth.getAccessToken(),\n  getUserId: () => auth.user?.id,\n  onVerified: () => setPremium(true),\n});\n\nawait iap.purchase(\"premium_monthly\");\niap.remove(); // çıkışta / unmount'ta\n```\n\n---\n\n## Teklifler (offers) ve promosyon kodları\n\nİki mağaza teklifleri farklı modeller: **Android'de her abonelik satın alması bir offer token\nister** (SKU bir kap; içinde base plan, deneme, indirimli giriş teklifi vardır), **iOS'ta ise**\nSKU ürünün kendisidir ve teklifler üstüne imzalı olarak eklenir. SDK bu farkı sizin yerinize\nkapatır.\n\n```ts\n// Android: belirli bir teklife yönlendir (yoksa ilk teklif otomatik seçilir)\nawait iap.purchase(\"premium_monthly\", { preferredOfferId: \"black-friday\" });\n\n// Android: elinizdeki token'ı doğrudan verin\nawait iap.purchase(\"premium_monthly\", { offerToken });\n\n// iOS: sunucunuzda imzalanmış promosyonel teklif (klasik dört alanlı biçim)\nawait iap.purchase(\"premium_monthly\", {\n  withOffer: { identifier, keyIdentifier, nonce, signature, timestamp },\n});\n\n// iOS 15+: aynı şeyin tek compact-JWS hâli\nawait iap.purchase(\"premium_monthly\", {\n  promotionalOfferJWS: { jws, offerId: \"winback10\" },\n});\n\n// iOS 18+: churn olmuş aboneyi geri kazanma teklifi\nawait iap.purchase(\"premium_monthly\", { winBackOfferId: \"comeback\" });\n\n// Tüketilebilir üründen birden fazla (iOS)\nawait iap.purchase(\"coins_100\", { quantity: 3 });\n\n// AB mevzuatı: fiyat bu kullanıcıya özel hesaplandıysa\nawait iap.purchase(\"premium_monthly\", { isOfferPersonalized: true });\n```\n\n**Promosyon / teklif kodları** için tek çağrı yeterlidir; iOS App Store kullanım sayfasını\nuygulama içinde açar, Android Play redeem sayfasını açar. Kullanılan kod normal satın alma\ndinleyicisinden gelir, yani ücretli satın alma gibi doğrulanır ve kaydedilir:\n\n```ts\nawait iap.presentOfferCodeRedemption();\n```\n\n`isEligibleForIntroOffer` ile paywall'da \"ücretsiz deneme\" yazıp yazmayacağınıza karar verin —\nuygun olmayan kullanıcıya bunu göstermek doğrudan destek talebi demektir:\n\n```ts\nconst eligible = await iap.isEligibleForIntroOffer(subscriptionGroupId);\n```\n\n---\n\n## Abonelik yükseltme / düşürme (Android)\n\n```ts\nawait iap.purchase(\"premium_yearly\", {\n  replacement: {\n    oldProductId: \"premium_monthly\",\n    replacementMode: \"with-time-proration\", // kalan süre kredilendirilir\n    purchaseToken: currentPurchaseToken,\n  },\n});\n```\n\niOS'ta aynı abonelik grubundaki başka bir ürünü satın almak yükseltme sayılır; ayrıca\nparametre gerekmez.\n\n---\n\n## Satın almanın kullanıcıya bağlanması\n\nGeçerli bir makbuz \"bir satın alma yapıldı\" demektir — \"bunu **bu** kullanıcı yaptı\" demez.\nMakbuzlar sızabilir. `getUserId` (veya `userId`) verdiğinizde SDK bunu store'a\n`appAccountToken` (iOS) / `obfuscatedAccountId` (Android) olarak iletir; store bu değeri\n**imzalı makbuzun içinde** geri döner ve backend eşleşmeyi kontrol eder. Eşleşmezse\ndoğrulama `409` ile reddedilir ve `onVerifyError` `reason: \"conflict\"` ile çağrılır.\n\n> iOS'ta `appAccountToken` bir **UUID** olmalıdır. Wappa `PublicUser` id'si zaten GUID'dir.\n\n---\n\n## Doğrulama hataları\n\n`onVerifyError(error, purchase)` tipli bir `reason` taşır:\n\n| `reason` | Anlamı | Transaction |\n| --- | --- | --- |\n| `conflict` | Makbuz başka bir hesaba ait (409) | kapatılır |\n| `rejected` | Store reddetti: sahte, iade edilmiş, süresi dolmuş, yanlış uygulama/ortam (400) | kapatılır |\n| `rate-limited` | Çok fazla deneme (429) | **açık bırakılır** |\n| `network` | Backend'e ulaşılamadı / 5xx | **açık bırakılır** |\n\nAçık bırakılan transaction'ı mağaza yeniden iletir, böylece backend tekrar dener. Kalıcı\nolarak reddedilenler ise kapatılır: aksi hâlde aynı ölü makbuz her açılışta yeniden gelir ve\nAndroid tarafında 3 gün onaylanmayan satın alma **otomatik iade** edilir.\n\n---\n\n## API\n\n### `useWappaPurchases(config) → hook`\n### `initWappaPurchases(config) → Promise<controller>`\n### `restorePurchases(config)` · `restorePurchasesDetailed(config)`\n\nController / hook üzerindeki metotlar:\n\n| Metot | Açıklama |\n| --- | --- |\n| `purchase(sku, options?)` | Satın alma başlatır (teklif, adet, yükseltme seçenekleriyle) |\n| `restore()` | Mevcut satın almaları backend'e yeniden doğrulatır |\n| `restoreDetailed()` | Aynısı + reddedilenleri sebebiyle birlikte döner |\n| `getProducts()` | Ürün/abonelik meta verisini yükler (yalnızca controller) |\n| `getActiveSubscriptions(skus?)` | Store'a göre aktif abonelikler (backend'e gitmez) |\n| `hasActiveSubscription(skus?)` | Hızlı \"abone mi?\" kontrolü |\n| `presentOfferCodeRedemption()` | Promosyon / teklif kodu ekranını açar |\n| `manageSubscriptions(sku?)` | Platformun abonelik yönetim ekranını açar |\n| `requestRefund(sku)` | iOS iade talebi akışı (Android'de `null`) |\n| `isEligibleForIntroOffer(groupId)` | iOS intro/deneme uygunluğu |\n| `getStorefront()` | Kullanıcının mağaza ülke kodu |\n| `remove()` | Dinleyicileri kapatır (yalnızca controller) |\n\n**config** (tümü opsiyonel):\n\n| Alan | Tip | Varsayılan | Açıklama |\n| --- | --- | --- | --- |\n| `siteKey` | `string` | `EXPO_PUBLIC_WAPPA_SITE_KEY` | Panel site anahtarı (`{site}`) |\n| `apiUrl` | `string` | hosted | Backend URL |\n| `language` | `string` | `en-us` | Rota dil segmenti |\n| `subscriptionSkus` | `string[]` | `[]` | Abonelik ürün id'leri (Plan `Code` ile eşleşmeli) |\n| `productSkus` | `string[]` | `[]` | Tek seferlik ürün id'leri |\n| `consumableSkus` | `string[]` | `[]` | Tüketilebilir ürünler (`isConsumable: true` ile biter) |\n| `getAccessToken` / `accessToken` | `() => string \\| Promise<string>` | — | Giriş yapan kullanıcının JWT'si |\n| `getUserId` / `userId` | `() => string \\| Promise<string>` | — | Makbuzu hesaba bağlar (`appAccountToken`) |\n| `autoFinishTransaction` | `boolean` | `true` | Doğrulama sonrası transaction'ı bitir |\n| `onVerified` | `(sub) => void` | — | Backend kaydı oluşturunca |\n| `onVerifyError` | `(error, purchase) => void` | — | Backend makbuzu reddedince (tipli sebep) |\n| `onPurchaseSuccess` | `(result) => void` | — | Satın alma + doğrulama sonrası |\n| `onPurchaseError` | `(error) => void` | — | Mağaza satın alma hatası |\n| `onError` | `(error) => void` | — | Doğrulama/ağ hatası |\n\n---\n\n## Notlar\n\n- Satın almalar yalnızca **gerçek cihaz + dev/standalone build**'de çalışır.\n- iOS'ta StoreKit 2 kullanılır; `purchaseToken` alanı doğrulama için JWS taşır. Android'de\n  Play purchase token'dır.\n- Yenilemeler/iptaller/iadeler, backend'in App Store Server Notifications V2 ve Google RTDN\n  webhook'larıyla panelde güncellenir; kaçan bildirimler arka plan mutabakatıyla telafi edilir.\n- **Restore işlemi transaction'ı bitirmez** — kullanıcı mevcut haklarını yeniden eşliyordur,\n  orada tüketmek hakkı yok ederdi.\n- SDK akışınızı bozacak exception fırlatmaz; hatalar `onError` / `onPurchaseError` /\n  `onVerifyError` ile bildirilir.\n\n## Lisans\n\nMIT\n","readmeFilename":"README.md","_rev":"1-d66c861f419ae4dbbef3aab12caa41f8"}