@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ürKurulum
npm install @appaflytech/wappa-inapp-purchase
npx expo install expo-iapapp.json içindeki plugins dizisine expo-iap'i ekleyin:
{ "expo": { "plugins": ["expo-iap"] } }
expo-iap(>=5),reactvereact-nativepeer 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.comPanel tarafında yapılması gerekenler
- 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.
- Her store ürünü için o organizasyonda bir Plan açın;
Kodalanını store'dakiproductIdile aynı yapın (örn.premium_monthly). - 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.
- iOS için Bundle ID (zorunlu) ve tercihen App Store Server API (.p8) bilgilerini girin.
- Android için Package Name + service-account JSON ve Pub/Sub audience girin.
- 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'taTeklifler (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
appAccountTokenbir UUID olmalıdır. WappaPublicUserid'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;
purchaseTokenalanı 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/onVerifyErrorile bildirilir.
Lisans
MIT
