| name | revenuecat-patterns |
| description | RevenueCat SDK entegrasyon pattern'leri. iOS (Swift), Android (Kotlin), React Native ve Flutter icin setup, offerings, entitlement checking, webhook integration, StoreKit 2 migration ve sandbox testing. |
RevenueCat Integration Patterns
SDK Setup
iOS (Swift + StoreKit 2)
import RevenueCat
Purchases.logLevel = .debug
Purchases.configure(
with: .init(withAPIKey: "appl_XXXXXXXXXXXXX")
.with(usesStoreKit2IfAvailable: true)
)
Purchases.shared.logIn("user_id_from_your_backend") { customerInfo, created, error in
}
Purchases.shared.logOut { customerInfo, error in }
Android (Kotlin)
Purchases.logLevel = LogLevel.DEBUG
Purchases.configure(
PurchasesConfiguration.Builder(this, "goog_XXXXXXXXXXXXX")
.appUserID("user_id")
.build()
)
React Native
import Purchases from 'react-native-purchases';
await Purchases.configure({
apiKey: Platform.OS === 'ios'
? 'appl_XXXXXXXXXXXXX'
: 'goog_XXXXXXXXXXXXX',
appUserID: userId ?? undefined,
});
Flutter
// main.dart
await Purchases.configure(
PurchasesConfiguration('appl_XXXXXXXXXXXXX')
..appUserID = userId
);
Offerings ve Paywalls
Offerings Getirme
Purchases.shared.getOfferings { offerings, error in
guard let current = offerings?.current else { return }
let monthly = current.monthly
let annual = current.annual
let weekly = current.package(identifier: "weekly")
if let monthly = monthly {
priceLabel.text = monthly.storeProduct.localizedPriceString
}
}
Purchases.sharedInstance.getOfferingsWith(
onError = { error -> },
onSuccess = { offerings ->
val current = offerings.current ?: return@getOfferingsWith
val monthly = current.monthly
val annual = current.annual
monthly?.product?.let { product ->
priceText.text = product.price.formatted
}
}
)
const offerings = await Purchases.getOfferings();
const current = offerings.current;
if (current) {
const monthly = current.monthly;
const annual = current.annual;
monthly?.product.priceString;
if (monthly && annual) {
const monthlyPerYear = monthly.product.price * 12;
const savings = Math.round((1 - annual.product.price / monthlyPerYear) * 100);
}
}
RevenueCat Paywalls (No-code)
import RevenueCatUI
PaywallView()
.onPurchaseCompleted { customerInfo in
}
.onDismiss {
}
PaywallFooterView()
PaywallDialog(
PaywallDialogOptions.Builder()
.setDismissRequest { }
.setListener(object : PaywallListener {
override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) {
}
})
.build()
)
Entitlement Checking
Purchases.shared.getCustomerInfo { customerInfo, error in
let isPremium = customerInfo?.entitlements["premium"]?.isActive == true
if let entitlement = customerInfo?.entitlements["premium"] {
entitlement.isActive
entitlement.willRenew
entitlement.expirationDate
entitlement.periodType
entitlement.productIdentifier
}
}
Purchases.shared.delegate = self
func purchases(_ purchases: Purchases,
receivedUpdated customerInfo: CustomerInfo) {
let isPremium = customerInfo.entitlements["premium"]?.isActive == true
updateUI(isPremium: isPremium)
}
const customerInfo = await Purchases.getCustomerInfo();
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
Purchases.addCustomerInfoUpdateListener((info) => {
const isPremium = info.entitlements.active['premium'] !== undefined;
setIsPremium(isPremium);
});
Feature Gating Pattern
class EntitlementManager {
private customerInfo: CustomerInfo | null = null;
async refresh(): Promise<void> {
this.customerInfo = await Purchases.getCustomerInfo();
}
get isPremium(): boolean {
return this.customerInfo?.entitlements.active['premium'] !== undefined;
}
get isOnTrial(): boolean {
const ent = this.customerInfo?.entitlements.active['premium'];
return ent?.periodType === 'TRIAL';
}
get trialEndDate(): Date | null {
const ent = this.customerInfo?.entitlements.active['premium'];
if (ent?.periodType !== ) ;
ent. ? (ent.) : ;
}
(): {
.?..[]?. ?? ;
}
}
Purchase Flow
Purchases.shared.purchase(package: monthlyPackage) { transaction, customerInfo, error, userCancelled in
if userCancelled {
return
}
if let error = error {
handleError(error)
return
}
if customerInfo?.entitlements["premium"]?.isActive == true {
unlockPremium()
}
}
try {
const { customerInfo } = await Purchases.purchasePackage(monthlyPackage);
if (customerInfo.entitlements.active['premium']) {
unlockPremium();
}
} catch (e: any) {
if (e.userCancelled) return;
handleError(e);
}
Restore Purchases (ZORUNLU - Apple Requirement)
Purchases.shared.restorePurchases { customerInfo, error in
if customerInfo?.entitlements["premium"]?.isActive == true {
showAlert("Aboneliginiz geri yuklendi!")
unlockPremium()
} else {
showAlert("Aktif abonelik bulunamadi.")
}
}
Webhook Integration (Server-Side)
Webhook Setup
RevenueCat Dashboard > Project > Integrations > Webhooks
- URL:
https://your-api.com/webhooks/revenuecat
- Authorization Header: Bearer token ile dogrula
Webhook Handler
import { NextRequest, NextResponse } from 'next/server';
interface RevenueCatEvent {
api_version: string;
event: {
type: string;
app_user_id: string;
product_id: string;
entitlement_ids: string[];
period_type: 'TRIAL' | 'NORMAL' | 'INTRO';
expiration_at_ms: number;
environment: 'SANDBOX' | 'PRODUCTION';
price_in_purchased_currency: number;
currency: string;
store: 'APP_STORE' | 'PLAY_STORE' | 'STRIPE';
};
}
const EVENT_HANDLERS: Record<string, (event: RevenueCatEvent['event']) => Promise<void>> = {
'INITIAL_PURCHASE': async (event) => {
await db..({
: { : event. },
: { : , : () }
});
analytics.(, {
: event.,
: event.,
: event.,
: event.,
: event.,
});
},
: (event) => {
db..({
: { : event. },
: { : () }
});
},
: (event) => {
db..({
: { : event. },
: { : , : () }
});
(event., event.);
},
: (event) => {
db..({
: { : event. },
: { : , : () }
});
},
: (event) => {
db..({
: { : event. },
: { : }
});
(event.);
},
: (event) => {
},
};
() {
authHeader = req..();
(authHeader !== ) {
.({ : }, { : });
}
: = req.();
(process.. === && body.. === ) {
.({ : });
}
handler = [body..];
(handler) {
(body.);
}
.({ : });
}
Sandbox Testing
iOS Sandbox
- App Store Connect > Users and Access > Sandbox Testers
- Yeni sandbox kullanici olustur (gercek email gerekli)
- iPhone Settings > App Store > Sandbox Account ile giris
- Sandbox'ta sureler kisaltilmis:
- 1 haftalik = 3 dakika
- 1 aylik = 5 dakika
- 1 yillik = 1 saat
- Auto-renew 6 kez sonra durur
Android Test
- Google Play Console > Setup > License testing
- Test email'lerini ekle
- Test track'e yukle (internal testing)
Purchases.logLevel = .debug ile test et
Debug Checklist
[ ] RevenueCat dashboard'da sandbox event'leri gorunuyor mu?
[ ] Entitlement dogru aktive oluyor mu?
[ ] Restore purchases calisiyor mu?
[ ] Webhook'lar geliyor mu? (RequestBin ile test et)
[ ] Fiyatlar locale'e gore formatlanmis mi?
[ ] Trial suresi dogru gorunuyor mu?
[ ] Iptal sonrasi erisim sure sonunda kapaniyor mu?
StoreKit 2 Migration Notlari
- RevenueCat v4+ StoreKit 2'yi otomatik destekler
usesStoreKit2IfAvailable: true ile etkinlestir
- StoreKit 2 avantajlari: real-time transaction updates, better receipt handling
- iOS 15+ gerektirir (iOS 14 icin StoreKit 1 fallback otomatik)
- Server-side receipt validation artik gerekli degil (SK2 bunu yapiyor)
Onemli Kurallar
- Restore Purchases butonu ZORUNLU (Apple reject eder yoksa)
- Fiyatlari localizedPriceString ile goster (hardcode ETME)
- Sandbox'ta test etmeden production'a cikma
- Webhook secret'i .env'de tut (hardcode ETME)
- BILLING_ISSUE event'ini handle et (grace period uygula)
- Trial bitis tarihini kullaniciya goster (seffaflik)
- Anonim -> kayitli kullanici gecisinde merge yap (veri kaybi olmasin)