Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/revenuecat-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-12 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✗→✓ | ▲ Improved | — | — |
| case-07 | ✗→✓ | ▲ Improved | — | — |
swift// AppDelegate.swift veya @main App import RevenueCat Purchases.logLevel = .debug // sandbox'ta acik tut Purchases.configure( with: .init(withAPIKey: "appl_XXXXXXXXXXXXX") .with(usesStoreKit2IfAvailable: true) ) // Kullanici kimlik eslestirme (auth sonrasi) Purchases.shared.logIn("user_id_from_your_backend") { customerInfo, created, error in // created: true ise yeni kullanici } // Logout Purchases.shared.logOut { customerInfo, error in }
kotlin// Application.onCreate() Purchases.logLevel = LogLevel.DEBUG Purchases.configure( PurchasesConfiguration.Builder(this, "goog_XXXXXXXXXXXXX") .appUserID("user_id") // null ise anonim .build() )
typescriptimport Purchases from 'react-native-purchases'; // App.tsx useEffect icinde await Purchases.configure({ apiKey: Platform.OS === 'ios' ? 'appl_XXXXXXXXXXXXX' : 'goog_XXXXXXXXXXXXX', appUserID: userId ?? undefined, // null = anonim });
dart// main.dart await Purchases.configure( PurchasesConfiguration('appl_XXXXXXXXXXXXX') ..appUserID = userId );
swift// iOS Purchases.shared.getOfferings { offerings, error in guard let current = offerings?.current else { return } let monthly = current.monthly // Package? let annual = current.annual // Package? let weekly = current.package(identifier: "weekly") // Custom // Fiyat gosterme if let monthly = monthly { priceLabel.text = monthly.storeProduct.localizedPriceString // "$9.99" (locale'e gore formatlanmis) } }
kotlin// Android Purchases.sharedInstance.getOfferingsWith( onError = { error -> /* PurchasesError */ }, 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 // "$9.99" } } )
typescript// React Native const offerings = await Purchases.getOfferings(); const current = offerings.current; if (current) { const monthly = current.monthly; const annual = current.annual; // Fiyat monthly?.product.priceString; // "$9.99" // Savings hesapla if (monthly && annual) { const monthlyPerYear = monthly.product.price * 12; const savings = Math.round((1 - annual.product.price / monthlyPerYear) * 100); // "Save 60%" } }
swift// iOS - RevenueCat Paywall UI import RevenueCatUI // SwiftUI PaywallView() .onPurchaseCompleted { customerInfo in // Satin alma basarili } .onDismiss { // Kullanici kapatti } // Footer mode (kendi UI'in + RC pricing) PaywallFooterView()
kotlin// Android - Paywall UI PaywallDialog( PaywallDialogOptions.Builder() .setDismissRequest { /* kapandi */ } .setListener(object : PaywallListener { override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) { // Basarili } }) .build() )
swift// iOS - Erisim kontrolu Purchases.shared.getCustomerInfo { customerInfo, error in let isPremium = customerInfo?.entitlements["premium"]?.isActive == true // Detayli bilgi if let entitlement = customerInfo?.entitlements["premium"] { entitlement.isActive // true/false entitlement.willRenew // otomatik yenilenecek mi entitlement.expirationDate // bitis tarihi entitlement.periodType // .normal, .trial, .intro entitlement.productIdentifier // hangi urun } } // Listener (real-time degisiklik) Purchases.shared.delegate = self func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) { let isPremium = customerInfo.entitlements["premium"]?.isActive == true updateUI(isPremium: isPremium) }
typescript// React Native const customerInfo = await Purchases.getCustomerInfo(); const isPremium = customerInfo.entitlements.active['premium'] !== undefined; // Listener Purchases.addCustomerInfoUpdateListener((info) => { const isPremium = info.entitlements.active['premium'] !== undefined; setIsPremium(isPremium); });
typescript// Merkezi erisim kontrolu 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 !== 'TRIAL') return null; return ent.expirationDate ? new Date(ent.expirationDate) : null; } get willRenew(): boolean { return this.customerInfo?.entitlements.active['premium']?.willRenew ?? false; } }
swift// iOS Purchases.shared.purchase(package: monthlyPackage) { transaction, customerInfo, error, userCancelled in if userCancelled { // Kullanici iptal etti - agresif olmadan geri don return } if let error = error { // Hata: odeme basarisiz, network vb. handleError(error) return } if customerInfo?.entitlements["premium"]?.isActive == true { // BASARILI - premium erisim ac unlockPremium() } }
typescript// React Native try { const { customerInfo } = await Purchases.purchasePackage(monthlyPackage); if (customerInfo.entitlements.active['premium']) { unlockPremium(); } } catch (e: any) { if (e.userCancelled) return; handleError(e); }
swift// iOS - MUTLAKA paywall'da "Restore Purchases" butonu olmali Purchases.shared.restorePurchases { customerInfo, error in if customerInfo?.entitlements["premium"]?.isActive == true { showAlert("Aboneliginiz geri yuklendi!") unlockPremium() } else { showAlert("Aktif abonelik bulunamadi.") } }
RevenueCat Dashboard > Project > Integrations > Webhooks
https://your-api.com/webhooks/revenuecattypescript// Next.js API Route 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'; }; } // Event tipleri ve aksiyonlar const EVENT_HANDLERS: Record<string, (event: RevenueCatEvent['event']) => Promise<void>> = { // Yeni satin alma 'INITIAL_PURCHASE': async (event) => { await db.user.update({ where: { id: event.app_user_id }, data: { isPremium: true, subscriptionStart: new Date() } }); await analytics.track('subscription_started', { userId: event.app_user_id, product: event.product_id, price: event.price_in_purchased_currency, currency: event.currency, periodType: event.period_type, }); }, // Yenileme 'RENEWAL': async (event) => { await db.user.update({ where: { id: event.app_user_id }, data: { subscriptionRenewedAt: new Date() } }); }, // Iptal (hemen degil, sure sonunda biter) 'CANCELLATION': async (event) => { await db.user.update({ where: { id: event.app_user_id }, data: { willCancel: true, cancelledAt: new Date() } }); // Win-back kampanyasi baslat await triggerWinBackCampaign(event.app_user_id, event.expiration_at_ms); }, // Sure doldu 'EXPIRATION': async (event) => { await db.user.update({ where: { id: event.app_user_id }, data: { isPremium: false, expiredAt: new Date() } }); }, // Odeme sorunu 'BILLING_ISSUE': async (event) => { await db.user.update({ where: { id: event.app_user_id }, data: { hasBillingIssue: true } }); // Grace period bilgilendirmesi await sendBillingIssueNotification(event.app_user_id); }, // Trial donusumu 'SUBSCRIBER_ALIAS': async (event) => { // Anonim kullanici -> kayitli kullanici eslestirme }, }; export async function POST(req: NextRequest) { const authHeader = req.headers.get('authorization'); if (authHeader !== `Bearer ${process.env.REVENUECAT_WEBHOOK_SECRET}`) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body: RevenueCatEvent = await req.json(); // Sandbox event'lerini filtrele (production'da) if (process.env.NODE_ENV === 'production' && body.event.environment === 'SANDBOX') { return NextResponse.json({ ok: true }); } const handler = EVENT_HANDLERS[body.event.type]; if (handler) { await handler(body.event); } return NextResponse.json({ ok: true }); }
Purchases.logLevel = .debug ile test et[ ] 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?usesStoreKit2IfAvailable: true ile etkinlestir| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.