npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@appaflytech/wappa-inapp-purchase

v0.0.1

Published

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

Downloads

20

Readme

wappa-inapp-purchase

Expo / React Native uygulamalarına App Store / Google Play satın almalarını ekleyen SDK. expo-iap 5.x üzerine kuruludur; satın alma tamamlanınca makbuzu Wappa backend'ine sunucu-taraflı doğrulatır ve Wappa panelinde kaydeder.

Abonelikler, tüketilebilir ürünler (jeton/can), kalıcı ürünler (reklam kaldırma), teklifler (intro / promosyonel / win-back / Play base plan), promosyon kodları, abonelik yükseltme–düşürme ve iade akışı desteklenir.

Kullanıcı satın alır  →  expo-iap (App Store / Google Play)
        →  POST {site}/subscriptions/{language}/iap/verify  (Bearer JWT)
        →  Wappa backend makbuzu Apple/Google'a SORAR (istemcinin makbuzu otorite değildir)
        →  bundle/package + ürün + ortam + iade/süre kontrolleri (hepsi fail-closed)
        →  satın alma kullanıcıya bağlanır (appAccountToken / obfuscatedAccountId)
        →  productId → Plan.Code eşleşir; abonelik açılır ya da tek seferlik hak verilir
        →  Wappa admin "Abonelikler" + "IAP Doğrulama Kayıtları" ekranlarında görünür

Kurulum

npm install @appaflytech/wappa-inapp-purchase
npx expo install expo-iap

app.json içindeki plugins dizisine expo-iap'i ekleyin:

{ "expo": { "plugins": ["expo-iap"] } }

expo-iap (>=5), react ve react-native peer dependency'dir. Satın alma test etmek için development / standalone build gerekir (Expo Go desteklemez).


Env değişkenleri

EXPO_PUBLIC_WAPPA_SITE_KEY=my-site
# opsiyonel — varsayılan hosted panele bağlanır
EXPO_PUBLIC_WAPPA_API=https://wappa-ui-api.appaflytech.com

Panel tarafında yapılması gerekenler

  1. IAP Ayarları ekranında bir Organizasyon seçin. Store ürün id'si ancak bu organizasyondaki planlara eşlenir — seçilmeden hiçbir satın alma kaydedilemez.
  2. Her store ürünü için o organizasyonda bir Plan açın; Kod alanını store'daki productId ile aynı yapın (örn. premium_monthly).
  3. Plan formunda Store ürün türü'nü doğru seçin (Abonelik / Tüketilebilir / Kalıcı). Store'lar bunu doğrulama sırasında söylemez, bu yüzden katalogda beyan edilir.
  4. iOS için Bundle ID (zorunlu) ve tercihen App Store Server API (.p8) bilgilerini girin.
  5. Android için Package Name + service-account JSON ve Pub/Sub audience girin.
  6. Ekrandaki webhook URL'lerini App Store Connect ve Play RTDN Pub/Sub push aboneliğine yapıştırın.

Kullanım

A) React hook (önerilen)

import { useWappaPurchases } from "@appaflytech/wappa-inapp-purchase";
import { useWappaAuth } from "@appaflytech/wappa-auth";

function Paywall() {
  const auth = useWappaAuth();

  const {
    connected,
    subscriptions,
    purchase,
    restore,
    presentOfferCodeRedemption,
    manageSubscriptions,
  } = useWappaPurchases({
    subscriptionSkus: ["premium_monthly", "premium_yearly"],
    consumableSkus: ["coins_100"],
    getAccessToken: () => auth.getAccessToken(), // doğrulama ucu auth'lu
    getUserId: () => auth.user?.id,              // makbuzu bu hesaba bağlar
    onVerified: (sub) => console.log("Kaydedildi:", sub.id),
    onVerifyError: (e) => {
      if (e.reason === "conflict") alert("Bu satın alma başka bir hesaba ait.");
    },
  });

  return (
    <>
      {subscriptions.map((s) => (
        <Button key={s.id} title={s.displayPrice} onPress={() => purchase(s.id)} />
      ))}
      <Button title="Satın almaları geri yükle" onPress={restore} />
      <Button title="Promosyon kodu kullan" onPress={presentOfferCodeRedemption} />
      <Button title="Aboneliği yönet" onPress={() => manageSubscriptions()} />
    </>
  );
}

B) Imperative

import { initWappaPurchases } from "@appaflytech/wappa-inapp-purchase";

const iap = await initWappaPurchases({
  subscriptionSkus: ["premium_monthly"],
  getAccessToken: () => auth.getAccessToken(),
  getUserId: () => auth.user?.id,
  onVerified: () => setPremium(true),
});

await iap.purchase("premium_monthly");
iap.remove(); // çıkışta / unmount'ta

Teklifler (offers) ve promosyon kodları

İki mağaza teklifleri farklı modeller: Android'de her abonelik satın alması bir offer token ister (SKU bir kap; içinde base plan, deneme, indirimli giriş teklifi vardır), iOS'ta ise SKU ürünün kendisidir ve teklifler üstüne imzalı olarak eklenir. SDK bu farkı sizin yerinize kapatır.

// Android: belirli bir teklife yönlendir (yoksa ilk teklif otomatik seçilir)
await iap.purchase("premium_monthly", { preferredOfferId: "black-friday" });

// Android: elinizdeki token'ı doğrudan verin
await iap.purchase("premium_monthly", { offerToken });

// iOS: sunucunuzda imzalanmış promosyonel teklif (klasik dört alanlı biçim)
await iap.purchase("premium_monthly", {
  withOffer: { identifier, keyIdentifier, nonce, signature, timestamp },
});

// iOS 15+: aynı şeyin tek compact-JWS hâli
await iap.purchase("premium_monthly", {
  promotionalOfferJWS: { jws, offerId: "winback10" },
});

// iOS 18+: churn olmuş aboneyi geri kazanma teklifi
await iap.purchase("premium_monthly", { winBackOfferId: "comeback" });

// Tüketilebilir üründen birden fazla (iOS)
await iap.purchase("coins_100", { quantity: 3 });

// AB mevzuatı: fiyat bu kullanıcıya özel hesaplandıysa
await iap.purchase("premium_monthly", { isOfferPersonalized: true });

Promosyon / teklif kodları için tek çağrı yeterlidir; iOS App Store kullanım sayfasını uygulama içinde açar, Android Play redeem sayfasını açar. Kullanılan kod normal satın alma dinleyicisinden gelir, yani ücretli satın alma gibi doğrulanır ve kaydedilir:

await iap.presentOfferCodeRedemption();

isEligibleForIntroOffer ile paywall'da "ücretsiz deneme" yazıp yazmayacağınıza karar verin — uygun olmayan kullanıcıya bunu göstermek doğrudan destek talebi demektir:

const eligible = await iap.isEligibleForIntroOffer(subscriptionGroupId);

Abonelik yükseltme / düşürme (Android)

await iap.purchase("premium_yearly", {
  replacement: {
    oldProductId: "premium_monthly",
    replacementMode: "with-time-proration", // kalan süre kredilendirilir
    purchaseToken: currentPurchaseToken,
  },
});

iOS'ta aynı abonelik grubundaki başka bir ürünü satın almak yükseltme sayılır; ayrıca parametre gerekmez.


Satın almanın kullanıcıya bağlanması

Geçerli bir makbuz "bir satın alma yapıldı" demektir — "bunu bu kullanıcı yaptı" demez. Makbuzlar sızabilir. getUserId (veya userId) verdiğinizde SDK bunu store'a appAccountToken (iOS) / obfuscatedAccountId (Android) olarak iletir; store bu değeri imzalı makbuzun içinde geri döner ve backend eşleşmeyi kontrol eder. Eşleşmezse doğrulama 409 ile reddedilir ve onVerifyError reason: "conflict" ile çağrılır.

iOS'ta appAccountToken bir UUID olmalıdır. Wappa PublicUser id'si zaten GUID'dir.


Doğrulama hataları

onVerifyError(error, purchase) tipli bir reason taşır:

| reason | Anlamı | Transaction | | --- | --- | --- | | conflict | Makbuz başka bir hesaba ait (409) | kapatılır | | rejected | Store reddetti: sahte, iade edilmiş, süresi dolmuş, yanlış uygulama/ortam (400) | kapatılır | | rate-limited | Çok fazla deneme (429) | açık bırakılır | | network | Backend'e ulaşılamadı / 5xx | açık bırakılır |

Açık bırakılan transaction'ı mağaza yeniden iletir, böylece backend tekrar dener. Kalıcı olarak reddedilenler ise kapatılır: aksi hâlde aynı ölü makbuz her açılışta yeniden gelir ve Android tarafında 3 gün onaylanmayan satın alma otomatik iade edilir.


API

useWappaPurchases(config) → hook

initWappaPurchases(config) → Promise<controller>

restorePurchases(config) · restorePurchasesDetailed(config)

Controller / hook üzerindeki metotlar:

| Metot | Açıklama | | --- | --- | | purchase(sku, options?) | Satın alma başlatır (teklif, adet, yükseltme seçenekleriyle) | | restore() | Mevcut satın almaları backend'e yeniden doğrulatır | | restoreDetailed() | Aynısı + reddedilenleri sebebiyle birlikte döner | | getProducts() | Ürün/abonelik meta verisini yükler (yalnızca controller) | | getActiveSubscriptions(skus?) | Store'a göre aktif abonelikler (backend'e gitmez) | | hasActiveSubscription(skus?) | Hızlı "abone mi?" kontrolü | | presentOfferCodeRedemption() | Promosyon / teklif kodu ekranını açar | | manageSubscriptions(sku?) | Platformun abonelik yönetim ekranını açar | | requestRefund(sku) | iOS iade talebi akışı (Android'de null) | | isEligibleForIntroOffer(groupId) | iOS intro/deneme uygunluğu | | getStorefront() | Kullanıcının mağaza ülke kodu | | remove() | Dinleyicileri kapatır (yalnızca controller) |

config (tümü opsiyonel):

| Alan | Tip | Varsayılan | Açıklama | | --- | --- | --- | --- | | siteKey | string | EXPO_PUBLIC_WAPPA_SITE_KEY | Panel site anahtarı ({site}) | | apiUrl | string | hosted | Backend URL | | language | string | en-us | Rota dil segmenti | | subscriptionSkus | string[] | [] | Abonelik ürün id'leri (Plan Code ile eşleşmeli) | | productSkus | string[] | [] | Tek seferlik ürün id'leri | | consumableSkus | string[] | [] | Tüketilebilir ürünler (isConsumable: true ile biter) | | getAccessToken / accessToken | () => string \| Promise<string> | — | Giriş yapan kullanıcının JWT'si | | getUserId / userId | () => string \| Promise<string> | — | Makbuzu hesaba bağlar (appAccountToken) | | autoFinishTransaction | boolean | true | Doğrulama sonrası transaction'ı bitir | | onVerified | (sub) => void | — | Backend kaydı oluşturunca | | onVerifyError | (error, purchase) => void | — | Backend makbuzu reddedince (tipli sebep) | | onPurchaseSuccess | (result) => void | — | Satın alma + doğrulama sonrası | | onPurchaseError | (error) => void | — | Mağaza satın alma hatası | | onError | (error) => void | — | Doğrulama/ağ hatası |


Notlar

  • Satın almalar yalnızca gerçek cihaz + dev/standalone build'de çalışır.
  • iOS'ta StoreKit 2 kullanılır; purchaseToken alanı doğrulama için JWS taşır. Android'de Play purchase token'dır.
  • Yenilemeler/iptaller/iadeler, backend'in App Store Server Notifications V2 ve Google RTDN webhook'larıyla panelde güncellenir; kaçan bildirimler arka plan mutabakatıyla telafi edilir.
  • Restore işlemi transaction'ı bitirmez — kullanıcı mevcut haklarını yeniden eşliyordur, orada tüketmek hakkı yok ederdi.
  • SDK akışınızı bozacak exception fırlatmaz; hatalar onError / onPurchaseError / onVerifyError ile bildirilir.

Lisans

MIT