Irbisa · Шпаргалка
Встроенные покупки во Flutter
in_app_purchase и RevenueCat, типы продуктов, поток покупок, восстановление, состояния подписки, триалы, серверные уведомления, правила сторов, тестирование
Приложение продаёт премиум-подписку, печатную фотокнигу и запись на йогу. Что обязано идти через биллинг App Store и Google Play, а что можно оплатить через Stripe?
Чем отличаются расходуемые и нерасходуемые покупки и автовозобновляемые подписки и что каждый тип обязывает делать приложение?
Пройдите по шагам покупки с плагином
in_app_purchase:queryProductDetails,buyNonConsumable,purchaseStreamиcompletePurchase. Что делает каждый шаг?final iap = InAppPurchase.instance;// In main(), before runApp: an update with no listener is lostvoid startPurchases() => iap.purchaseStream.listen(_onPurchases); Future<ProductDetails?> loadPremium() async { if (!await iap.isAvailable()) return null; // no store on this device…queryProductDetailsна реальном устройстве возвращает все идентификаторы вnotFoundIDs. Что вы проверите в каждом сторе?Найдите баг: покупка, сделанная прямо перед тем, как приложение убили, ничего не открывает, а через три дня Google Play возвращает за неё деньги.
class _PaywallState extends State<Paywall> { late final StreamSubscription<List<PurchaseDetails>> _sub; @override void initState() { super.initState(); _sub = InAppPurchase.instance.purchaseStream.listen((purchases) async { for (final p in purchases.where((p) => p.status == PurchaseStatus.purchased)) { await api.grant(p.verificationData.serverVerificationData); await InAppPurchase.instance.completePurchase(p); } }); } @override void dispose() { _sub.cancel(); super.dispose(); } @override Widget build(BuildContext context) => const PlanList(); // Buy: buyNonConsumable}Что означает
PurchaseStatus.pending(Ask to Buy, оплата наличными) и что должен делать пейволл, пока покупка остаётся в ожидании?enum PaywallStatus { idle, buying, waiting, owned } final paywallStatus = ValueNotifier(PaywallStatus.idle); Future<void> onPurchase(PurchaseDetails p) async { switch (p.status) {…Найдите баг: после нестабильного соединения у пользователя, который один раз купил 500 монет, на балансе 1000.
// Dio has a retry interceptor: a timed-out request is sent again, up to 3 timesFuture<void> onPurchase(PurchaseDetails p) async { if (p.status != PurchaseStatus.purchased) return; final res = await dio.post('/wallet/add', data: {'coins': 500}); wallet.balance = res.data['balance'] as int; await InAppPurchase.instance.completePurchase(p);}Как реализовать Restore Purchases на iOS и Android и почему App Review отклоняет платное приложение без возможности восстановить покупки?
Future<void> onRestoreTapped() async { final iap = InAppPurchase.instance; if (Platform.isIOS) { try { // AppStore.sync(): may ask for the Apple Account password, so only on a tap await iap.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>().sync();…Как приложение при запуске узнаёт, что подписка ещё активна, и что показывать без сети или когда ваш сервер лежит?
class EntitlementRepository { EntitlementRepository(this._api, this._cache); final Api _api; final EntitlementCache _cache; /// First frame, no network: the server's last answer, until its period ends.…in_app_purchaseилиpurchases_flutterот RevenueCat: что RevenueCat снимает с вас и чего это стоит?Future<void> initPurchases(String userId) async { final config = PurchasesConfiguration( Platform.isIOS ? 'appl_xxx' : 'goog_xxx', // public SDK keys, safe to ship )..appUserID = userId; // your account id: the same customer on every platform await Purchases.configure(config); Purchases.addCustomerInfoUpdateListener(_apply); // renewals, refunds, restores…Этот пейволл собирает подпись с ценой из
rawPrice. Что видят пользователи за пределами США и как правильно показывать цену?// yearly: the ProductDetails for 'pro_yearly' from queryProductDetailsWidget yearlyPriceLabel(ProductDetails yearly) { final perMonth = yearly.rawPrice / 12; return Text('Just \$${perMonth.toStringAsFixed(2)} a month');}Как предложить 7-дневный бесплатный период только тем, кому он положен, и в App Store, и в Google Play?
/// The entry to sell with a trial, or null: then the paywall shows no trial copy.Future<ProductDetails?> trialProduct(List<ProductDetails> details) async { final play = details.whereType<GooglePlayProductDetails>().toList(); if (play.isNotEmpty) { // Android: one entry per offer, and Play lists only offers this user may buy return play.where((d) {…У подписчика истекла карта до продления. Через какие состояния (grace period, billing retry, account hold, expired) он пройдёт и что показывает приложение в каждом?
Продления, отмены и возвраты происходят, пока приложение закрыто. Как о них узнаёт бэкенд и почему сообщать о них не может само приложение?
Как тестировать покупки, не тратя денег: sandbox-аккаунты, файлы конфигурации StoreKit, лицензионные тестировщики Play, и чего не воспроизведёт ни один из них?
Пользователь оформил подписку на iPhone, а потом вошёл в ваше Android-приложение и на сайт. Где живёт подписка и как о ней узнаёт каждая платформа?
// accountId: a UUID your backend gives the user at sign-up and never changesFuture<bool> subscribe(ProductDetails plan, String accountId) => InAppPurchase.instance.buyNonConsumable( purchaseParam: PurchaseParam( productDetails: plan, // iOS: becomes StoreKit's appAccountToken (dropped silently unless a UUID);…Как перевести подписчика с месячного плана на годовой в обоих сторах и что происходит со старой подпиской и уже уплаченными деньгами?
Future<void> switchToYearly(ProductDetails yearly, PurchaseDetails current) async { final iap = InAppPurchase.instance; if (current is GooglePlayPurchaseDetails) { // Android: name the purchase being replaced, or Play may sell a second one await iap.buyNonConsumable( purchaseParam: GooglePlayPurchaseParam(…Продакт хочет протестировать три дизайна пейволла и две цены. Что нужно в приложении и на сервере и почему нельзя менять цену живого продукта для одного пользователя?