Mua hàng trong ứng dụng (in-app purchases) React Native 2026 được triển khai gọn nhất bằng RevenueCat (react-native-purchases v8+), SDK này bọc StoreKit 2 trên iOS và Google Play Billing v7 trên Android sau một API thống nhất, đồng thời xử lý xác thực receipt, quản lý entitlement và đồng bộ subscription đa nền tảng. Nếu bạn đến React Native từ web, hãy hình dung RevenueCat như Stripe cho mobile: bạn gọi vài hook, dashboard lo phần backend, còn StoreKit 2 và Google Play Billing chỉ là "payment processor" bên dưới. Honestly, lần đầu tôi ship IAP cho một app Expo, tôi đã mất nguyên một tuần loay hoay với receipt validation trước khi chuyển sang RevenueCat. Bài viết này hướng dẫn cài đặt, cấu hình product, viết paywall, test sandbox và xử lý webhook hoàn chỉnh trên Expo SDK 55.
expo-in-app-purchases đã bị deprecated từ 2023. Năm 2026 chỉ còn hai lựa chọn chính thức là react-native-purchases (RevenueCat) và expo-iap (kế thừa react-native-iap).
RevenueCat v8 yêu cầu React Native ≥ 0.73, iOS ≥ 13.4, Android minSdk 23 và Google Play Billing Library v7. Version cũ hơn sẽ bị Play Console từ chối upload từ tháng 8/2025.
StoreKit 2 (iOS 15+) thay thế hoàn toàn StoreKit 1 với async/await API, JWS-signed transactions và không còn cần receipt validation thủ công qua server cũ.
Bạn không thể test purchase thật trong Expo Go. Phải build development client qua EAS Build hoặc local prebuild, dùng StoreKit Configuration trên iOS Simulator và license testers trên Android.
Entitlement của RevenueCat (ví dụ "pro") là lớp trừu tượng giúp bạn đổi product ID, tăng giá, A/B test mà không phải release app lại.
Server-side webhook là bắt buộc nếu bạn cấp quyền dựa trên subscription, đừng tin hoàn toàn vào client.
Chọn thư viện IAP cho React Native năm 2026
Hệ sinh thái IAP trong React Native đã đơn giản hơn nhiều so với hai năm trước. expo-in-app-purchases bị Expo gỡ khỏi danh sách thư viện khuyến nghị, và bản npm gần nhất đã ba năm tuổi. react-native-iap cũng được tác giả gốc chuyển thành module Expo có tên mới là expo-iap, để đồng nhất với Expo Modules API và hỗ trợ Continuous Native Generation. Cuối cùng, hai lựa chọn còn lại là RevenueCat (react-native-purchases) và expo-iap.
Nếu bạn từng làm Stripe trên React web, sự lựa chọn giống như giữa "Stripe Checkout managed" và "Stripe Elements + tự xây backend". RevenueCat tương đương loại thứ nhất: bạn không phải viết server xác thực receipt, không phải xử lý gia hạn, không phải lo lệch entitlement giữa iOS, Android và web. expo-iap giống loại thứ hai, bạn có quyền kiểm soát hoàn toàn nhưng phải tự xây backend, tự lưu trữ transaction, tự đồng bộ trạng thái subscription cho user logged in trên nhiều thiết bị.
Tiêu chí
RevenueCat (react-native-purchases)
expo-iap
Backend bao gồm
Có (xác thực receipt, entitlement, analytics)
Không, bạn tự xây
Hỗ trợ web (Stripe)
Có (Web Billing)
Không, chỉ iOS/Android
Paywall UI dựng sẵn
Có (react-native-purchases-ui)
Không
StoreKit 2 / Play Billing v7
Bọc sẵn
Truy cập trực tiếp
Chi phí
Miễn phí đến $2.5K MTR, sau đó 1%
Hoàn toàn miễn phí
Expo Go preview
Có Preview API Mode
Không
Phù hợp khi
Subscription, đa nền tảng, cần analytics
Mua một lần, single-platform, MVP nhỏ
Cài đặt RevenueCat trong dự án Expo
Phần này giả định bạn đã có project Expo SDK 55 với EAS Build cấu hình sẵn cho App Store và Google Play. RevenueCat là native module, nghĩa là bạn không thể chạy trong Expo Go. Bắt buộc phải tạo development build.
# 1. Cài SDK chính
npx expo install react-native-purchases react-native-purchases-ui
# 2. Prebuild để sinh thư mục ios/ và android/
npx expo prebuild --clean
# 3. Build dev client
eas build --profile development --platform ios
eas build --profile development --platform android
Sau khi cài, khởi tạo SDK càng sớm càng tốt trong vòng đời app. Đặt trong _layout.tsx của Expo Router là vị trí tự nhiên nhất:
// app/_layout.tsx
import { useEffect } from 'react';
import { Platform } from 'react-native';
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
import { Slot } from 'expo-router';
const API_KEY_IOS = process.env.EXPO_PUBLIC_RC_IOS_KEY!;
const API_KEY_ANDROID = process.env.EXPO_PUBLIC_RC_ANDROID_KEY!;
export default function RootLayout() {
useEffect(() => {
Purchases.setLogLevel(__DEV__ ? LOG_LEVEL.DEBUG : LOG_LEVEL.ERROR);
Purchases.configure({
apiKey: Platform.OS === 'ios' ? API_KEY_IOS : API_KEY_ANDROID,
// Gắn user id của bạn (auth provider). Nếu chưa login, để undefined,
// RevenueCat sẽ sinh anonymous id và bạn có thể alias sau.
appUserID: undefined,
});
}, []);
return <Slot />;
}
Hai API key riêng cho iOS và Android lấy trong RevenueCat Dashboard, vào Project settings → API keys. Đừng commit chúng vào git; dùng EXPO_PUBLIC_* để inject qua EAS Secrets. Nếu user đã đăng nhập (Auth0, Clerk, Supabase…), gọi Purchases.logIn(userId) sau khi auth state ready để gắn entitlement với chính user đó trên mọi thiết bị.
Cấu hình product trên App Store Connect và Google Play
Trước khi paywall hiện được giá, bạn phải khai báo product ở ba nơi: App Store Connect, Google Play Console và RevenueCat Dashboard. Lưu ý phân biệt ba khái niệm:
Product: định nghĩa ở mỗi store, ví dụ pro_monthly giá 99.000₫/tháng.
Offering: nhóm product mà user có thể chọn trên một paywall, ví dụ "Monthly", "Annual", "Lifetime".
Entitlement: quyền truy cập trừu tượng, ví dụ pro. Nhiều product cùng cấp một entitlement.
Lớp entitlement là lý do RevenueCat đáng tiền. Khi bạn tăng giá hoặc đổi product ID, code app không cần đổi vì paywall vẫn chỉ check customerInfo.entitlements.active["pro"]. Cách tách hook khỏi config này quen thuộc với ai từng dùng React Context hoặc feature-flag service trên web.
Bước trên App Store Connect
Vào My Apps → App → In-App Purchases → +, tạo Auto-Renewable Subscription, group pro_access.
Đặt Product ID dạng reverse-DNS: com.yourapp.pro.monthly. ID không thể đổi sau khi tạo.
Tạo Subscription Group "Pro Access", thêm nhiều tier (monthly/annual) trong cùng group. App Store sẽ tự xử lý upgrade/downgrade.
Cung cấp screenshot review, mô tả, giá. Trạng thái phải là "Ready to Submit" mới fetch được trong sandbox.
Bước trên Google Play Console
Upload ít nhất 1 internal testing build có chứa com.android.vending.BILLING permission (RevenueCat SDK tự thêm).
Vào Monetize → Products → Subscriptions → Create subscription, tạo base plan và offer.
Product ID nên trùng với iOS nếu có thể để Single Source of Truth. RevenueCat cho phép map riêng nếu khác.
Kích hoạt subscription (state Active). Google không trả product nếu còn Draft.
Bước trên RevenueCat Dashboard
Tạo Product cho iOS và Android, dán Product ID khớp chính xác.
Tạo Entitlement pro, gán cả hai product vào.
Tạo Offering "default" (RevenueCat tự fetch nếu tên là "default") với các package $rc_monthly, $rc_annual.
Viết paywall với hook usePurchases
Từ phiên bản react-native-purchases 8.x trở đi, RevenueCat đẩy mạnh hook API thay vì gọi imperative, cực kỳ tự nhiên cho ai đã quen TanStack Query trên web. Đây là một paywall tối giản fetch offerings và render giá:
Trường product.priceString đã được store địa phương hoá (ví dụ "99.000 ₫" hoặc "$0.99"), nên đừng bao giờ hard-code giá. Nếu bạn muốn một paywall đẹp dựng sẵn, gói react-native-purchases-ui cung cấp component <RevenueCatUI.Paywall /> render trực tiếp template config trên dashboard, đổi A/B copy mà không cần release app mới. Một dự án của tôi đã tăng conversion khoảng 18% chỉ bằng cách đổi headline trên dashboard, không hề chạm tới code.
Xử lý mua, restore và lỗi thường gặp
StoreKit 2 và Google Play Billing v7 trả về kết quả qua các state khác nhau, nhưng RevenueCat đã chuẩn hoá thành một object customerInfo duy nhất. Hai phương thức bạn sẽ dùng nhiều nhất ngoài purchasePackage là restorePurchases và listener addCustomerInfoUpdateListener:
Hook này nên gắn ở root layout để cả app cùng phản ứng. Khi user mua xong hoặc khi listener firing từ App Store (auto-renewal, refund), state cập nhật ngay, giống cách bạn dùng onAuthStateChanged trên web. Đối với nút "Khôi phục giao dịch", bắt buộc theo guideline Apple, chỉ cần gọi Purchases.restorePurchases() và xử lý cùng listener.
Một số mã lỗi RevenueCat quan trọng cần xử lý rõ ràng:
PURCHASE_CANCELLED: kiểm tra e.userCancelled, không hiện alert.
PAYMENT_PENDING: phổ biến trên Android khi dùng gift card, hãy chờ webhook cập nhật.
PRODUCT_ALREADY_PURCHASED: gợi ý user "Khôi phục" thay vì mua lại.
NETWORK_ERROR: retry với exponential backoff thay vì alert ngay.
Trong dự án gần nhất, tôi đã bỏ qua PAYMENT_PENDING và nhận về một loạt ticket support từ user trả qua gift card. Một bug nho nhỏ nhưng làm team support tốn cả tuần. Nếu app của bạn có content nhạy cảm sau paywall, bạn có thể kết hợp với Face ID hoặc Touch ID trước khi mở quyền, vừa thêm lớp bảo mật, vừa giúp parent confirm khi trẻ em mua.
Test sandbox: StoreKit Configuration và license tester
Test IAP là phần khiến nhiều dev mới chuyển từ web qua React Native bối rối nhất. Khác với Stripe test mode bật bằng API key, sandbox của App Store/Google Play đòi hỏi cấu hình tài khoản và build đúng cách. May là StoreKit 2 đem lại trải nghiệm tốt hơn nhiều so với StoreKit 1.
iOS: StoreKit Configuration file
StoreKit Configuration là file local mô phỏng product mà không cần lên App Store Connect, cực kỳ tiện khi prototype paywall trên simulator. Trong Xcode chọn File → New → File → StoreKit Configuration File, đặt tên Products.storekit, thêm Auto-Renewable Subscription với ID khớp App Store Connect. Sau đó:
Chọn scheme → Edit Scheme → Run → Options.
Mục StoreKit Configuration chọn file vừa tạo.
Build lại. RevenueCat SDK sẽ trả về product từ file local.
Để test purchase thật trên thiết bị, vào App Store Connect → Users and Access → Sandbox Testers, tạo tài khoản sandbox (email chưa từng dùng cho Apple ID), trên iPhone đăng nhập tài khoản đó trong Settings → App Store → Sandbox Account. Apple hiện rút ngắn thời lượng subscription test xuống còn 3 phút/tháng để bạn dễ test renewal flow. Tham khảo tài liệu chính thức của Apple về testing IAP trong sandbox để biết các quy tắc về reset tài khoản và intro offer.
Android: License Testers
Trên Google Play, mở Setup → License testing, thêm Gmail của bạn vào danh sách tester. Build phải được upload (kể cả internal track) và Gmail của thiết bị phải khớp tester. Test purchase miễn phí, gia hạn cũng được tăng tốc (ví dụ subscription 1 tháng renew sau 5 phút). Chi tiết khác có trong hướng dẫn test của Google Play Billing.
Webhook và cấp quyền phía server
Quy tắc bất di bất dịch: không tin client. Nếu API của bạn trả nội dung premium, hãy check entitlement ở server, không phải ở app. Cách an toàn nhất là dùng RevenueCat Webhooks để đồng bộ trạng thái subscription về database của bạn.
Webhook fire trong vài giây sau khi App Store hoặc Play Store thông báo cho RevenueCat, kể cả các sự kiện gián tiếp như refund, billing retry hay grace period. Nếu bạn xây feature gating ở backend (REST hoặc tRPC), đoạn proActive ở DB là source of truth. Client chỉ dùng customerInfo để render UI nhanh, còn quyền truy cập API thật vẫn check ở server.
Với app phức tạp dùng Expo Router và protected routes, bạn có thể wrap nhóm route premium trong một layout (pro)/_layout.tsx kiểm tra hook useEntitlement('pro') và redirect về /paywall nếu chưa có quyền. Pattern này giống middleware.ts của Next.js, chỉ là chạy trên client.
Câu hỏi thường gặp
RevenueCat có miễn phí không và khi nào phải trả tiền?
RevenueCat miễn phí cho đến khi app đạt $2.500 Monthly Tracked Revenue (MTR). Vượt ngưỡng, plan Starter tính 1% trên doanh thu phát sinh thêm. Hầu hết indie và startup giai đoạn đầu sẽ ở trong tier miễn phí khá lâu.
Tôi có cần backend riêng nếu đã dùng RevenueCat?
Không bắt buộc cho phần purchase logic, RevenueCat đã lo receipt validation và entitlement. Nhưng nếu API của bạn trả nội dung premium, bạn vẫn cần webhook đẩy trạng thái về DB để check quyền phía server, vì client luôn có thể bị giả mạo.
Có thể test in-app purchase trên Expo Go không?
Không. Expo Go không hỗ trợ native module purchase. RevenueCat có Preview API Mode trả product giả lập để bạn xem UI, nhưng phải tạo development build qua EAS Build để test sandbox thật trên App Store hoặc Google Play.
Sự khác nhau giữa StoreKit 2 và StoreKit 1?
StoreKit 2 (iOS 15+) dùng async/await, trả về Transaction đã được JWS-signed nên không cần gọi server xác thực receipt như trước. Nó cũng có API Transaction.currentEntitlements giúp restore nhanh hơn. RevenueCat v8 ưu tiên StoreKit 2 mặc định.
Google Play Billing v7 thay đổi gì so với v6?
Billing v7 tách subscription thành base plans và offers, hỗ trợ installment plans cho thị trường mới nổi, và bắt buộc khai báo prepaid plans riêng. Từ tháng 8/2025 Google Play Console từ chối upload app dùng Billing v5/v6. RevenueCat v8 đã upgrade sẵn, bạn chỉ cần đảm bảo không có dependency native khác kéo về version cũ.
Hướng dẫn migrate React Native từ kiến trúc cũ sang Fabric, TurboModules và Bridgeless Mode trên RN 0.83 (2026). Code examples, footgun và cách đo hiệu năng từ kinh nghiệm sáu app production.
Bộ ba EAS Build, EAS Submit và EAS Update giúp bạn deploy React Native lên App Store và Google Play năm 2026 mà không cần Mac riêng. Hướng dẫn cấu hình eas.json, runtimeVersion, OTA và CI/CD GitHub Actions với ví dụ thực tế.
Hướng dẫn triển khai push notifications React Native 2026 với expo-notifications, FCM v1 HTTP API và APNs HTTP/2: cấu hình credentials, đăng ký token, background handler, deep linking và troubleshooting các lỗi thường gặp.