Skip to content
ExponaBLOG
FLUTTER · DAY 069 / 100

App store payment rules: Flutter lessons from production

Know them before you build checkout.

Expona Admin8 min read

Six months into my ride-hailing platform, I shipped a checkout flow, got rejected from the App Store within 24 hours, and had to rebuild payment handling in a weekend. App store payment rules aren't a nice-to-know—they're a hard constraint that affects your entire monetization strategy. Let me walk you through what I learned so you don't repeat my mistakes.

Why app store payment rules matter

When you're building a Flutter app that generates revenue, you're not just following payment best practices—you're following Apple's and Google's legal and commercial requirements. These platforms take a 15–30% commission on every transaction processed through their payment systems. That's their leverage, and they enforce it strictly. Violating these rules doesn't just mean rejection; it can mean account suspension, app removal, or even developer account termination.

I've seen it happen. A travel booking app I reviewed tried to hide external payment links in a settings screen. The app was pulled within weeks. The developer had to rewrite the entire checkout flow using in-app purchases, adding months to their roadmap.

Understanding app store payment rules before you architect your checkout isn't bureaucratic overhead—it's foundational infrastructure work. You need to know what you can and can't do before you write a single line of payment logic.

Digital content usually requires in-app purchase

If your app sells digital goods—subscriptions, premium features, virtual currency, ebooks, courses, or anything that exists only within your app—you must use the app store's in-app purchase system. Apple calls this "digital content." Google has similar rules.

This is non-negotiable. I learned this when building a SaaS product's mobile tier. We wanted to let users upgrade from free to premium directly through an external payment provider. We couldn't. We had to integrate with StoreKit 2 (iOS) and Google Play Billing (Android).

Here's a minimal example of how you'd implement this in Flutter using the in_app_purchase package:

dart
import 'package:in_app_purchase/in_app_purchase.dart';

class PremiumSubscription {
  static const String premiumMonthlyId = 'com.example.app.premium_monthly';
  
  final InAppPurchase _iap = InAppPurchase.instance;
  
  Future initializePurchases() async {
    final bool available = await _iap.isAvailable();
    if (!available) {
      print('In-app purchases not available');
      return;
    }
    
    // Listen to purchase updates
    _iap.purchaseStream.listen(
      (List purchases) {
        _handlePurchaseUpdates(purchases);
      },
    );
  }
  
  Future buyPremium() async {
    final ProductDetailsResponse response = await _iap.queryProductDetails(
      {premiumMonthlyId},
    );
    
    if (response.productDetails.isEmpty) {
      print('Product not found');
      return;
    }
    
    final ProductDetails productDetails = response.productDetails.first;
    final PurchaseParam purchaseParam = PurchaseParam(
      productDetails: productDetails,
    );
    
    await _iap.buyNonConsumable(purchaseParam: purchaseParam);
  }
  
  void _handlePurchaseUpdates(List purchases) {
    for (final PurchaseDetails purchase in purchases) {
      if (purchase.status == PurchaseStatus.purchased) {
        _verifyAndGrantAccess(purchase);
      }
      
      if (purchase.pendingCompleteMark) {
        await _iap.completePurchase(purchase);
      }
    }
  }
}

Notice that we're never touching external payment APIs directly. The app store handles the payment, validates it, and delivers it. Your backend receives a receipt token that you verify server-side against Apple or Google's receipt validation APIs.

Common digital content that requires in-app purchase:

  • Subscription tiers (premium access, ad removal)
  • Virtual currency (coins, gems, tokens within the app)
  • Unlockable features or levels
  • Premium content (courses, ebooks, articles)
  • Extended app functionality

Physical goods can use external payments

Now here's where it gets practical. If you're selling physical goods—groceries, clothes, tickets to an event, food delivery—you can use external payment providers. Stripe, PayPal, Razorpay, whatever you want. The app store doesn't take a cut.

On my e-commerce platform, we integrated Stripe directly for checkout. We didn't use in-app purchase because customers were buying physical products that we shipped. Apple explicitly allows this, and we saved the commission.

The catch: you can't drive the customer from your app to an external payment system using buttons or UI prompts. You can't have a "Pay with Stripe" button in your app. Instead, you open a web view or send users to a mobile website where they complete checkout, then redirect back to your app.

Here's a pattern I use for physical goods checkout:

dart
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';

class PhysicalGoodsCheckout {
  Future initiateCheckout(String orderId) async {
    // Generate a secure checkout URL on your backend
    // Include session tokens, order details, and a redirect URI
    final String checkoutUrl = 'https://yourdomain.com/checkout?order=$orderId&app_session=${_generateSessionToken()}';
    
    // Open in web view (preferred method for better UX)
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => WebViewCheckout(url: checkoutUrl),
      ),
    );
  }
}

class WebViewCheckout extends StatefulWidget {
  final String url;
  
  const WebViewCheckout({required this.url});
  
  @override
  State createState() => _WebViewCheckoutState();
}

class _WebViewCheckoutState extends State {
  late WebViewController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onNavigationRequest: (NavigationRequest request) {
            // Listen for redirect back to app after payment
            if (request.url.startsWith('myapp://checkout/success')) {
              Navigator.pop(context, {'success': true});
              return NavigationDecision.prevent;
            }
            if (request.url.startsWith('myapp://checkout/cancel')) {
              Navigator.pop(context, {'success': false});
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      )
      ..loadRequest(Uri.parse(widget.url));
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Checkout')),
      body: WebViewWidget(controller: _controller),
    );
  }
}

This approach keeps the app store happy because:

  • You're not promoting external payment within the app UI
  • The user journey feels native (web view integrated)
  • Payment processing happens server-side, securely
  • You retain full payment control and avoid commissions

Rules differ by region

App store payment rules aren't uniform globally. China, the EU, South Korea, and Japan have different requirements. This is something I didn't account for in my first SaaS launch, and it cost me.

Some key regional differences:

  • European Union (Digital Markets Act): Apps must allow users to use external payment methods for digital goods. Apple introduced alternative app stores and payment options in the EU starting in 2024.
  • China (App Store): Apple requires in-app purchase, but the Chinese App Store operates under stricter content and payment rules. International payment providers often can't operate there.
  • South Korea: Regulators have pushed Apple and Google to allow alternative billing methods. Apps must offer multiple payment options.
  • Japan: Generally follows Apple's global rules, but local telco billing (carrier billing) is a common alternative that provides good user experience.

If you're building for a global audience, you need to handle this in code. A common pattern is to show different checkout flows based on user location:

dart
class CheckoutFlowSelector {
  Future selectCheckoutMethod(String userRegion, String productType) async {
    // For digital goods
    if (productType == 'digital') {
      if (userRegion == 'EU') {
        // Offer both in-app purchase AND external payment
        _showPaymentMethodSelection();
      } else {
        // In-app purchase only
        await _initiateInAppPurchase();
      }
    }
    
    // For physical goods
    if (productType == 'physical') {
      // Always use external payment (web checkout)
      await _initiateWebCheckout();
    }
  }
  
  Future detectUserRegion() async {
    // Use device locale, IP geolocation, or user preference
    final String? countryCode = Platform.localeName.split('_').last;
    return countryCode ?? 'US';
  }
}

Before you launch, check the app store guidelines for your target regions. The rules are published but frequently updated. A region-specific rule change can invalidate your entire payment architecture.

Read current guidelines before launch

This sounds obvious, but it's worth emphasizing: the app store guidelines change, and you won't get a notification. I've had apps pass review with a payment flow, then get flagged months later when guidelines tightened.

Before you submit:

  1. Read Apple's App Store Review Guidelines (section 3.1 covers payments). Visit developer.apple.com.
  2. Read Google's Google Play Billing Library policies and Payments Center requirements. Visit developer.android.com.
  3. Check the current version of both. I bookmark these pages and check them quarterly.
  4. If your app is international, read region-specific guidance (search for "App Store EU" or "Google Play regional policies").
  5. When in doubt, reach out to review support before submitting. Both platforms have support channels.

Here's what I do in my pre-launch checklist:

  1. Document every payment flow in my app (what triggers payment, which products use which method)
  2. Cross-reference each flow against the current guidelines
  3. Take screenshots of the specific guideline sections
  4. Test the entire payment path (purchase, refund, restore, error handling)
  5. Submit a test build for review with payment flows enabled
  6. Keep a changelog of any guideline updates I implement

Common mistakes

Mistake 1: Trying to hide external payment links. Some developers try to sneak payment links into settings, deep menu items, or obfuscated buttons. The review team finds these. They reject apps. Don't do it.

Mistake 2: Assuming physical goods rules apply globally. A ticketing app I reviewed used external payment for tickets in the US, then got flagged in Australia. Check regional rules per product type, not just product type globally.

Mistake 3: Not testing receipt validation. In-app purchases require server-side receipt verification. If your backend can't validate receipts, you're open to fraud and your app will get flagged after launch. Test this thoroughly.

Mistake 4: Launching without reading the latest guidelines. I submitted an app using a payment flow that was explicitly disallowed in guidelines updated two weeks prior. I didn't check. Rejection. Rebuild. Resubmit. Two-week delay.

Mistake 5: Mixing payment methods without clear separation. If you support both in-app and external payments (allowed in some regions), make sure the UI doesn't confuse users about which method they're using and which fees apply.

Key takeaways

  • Digital content (subscriptions, features, virtual goods) requires in-app purchase on iOS and Android. This is non-negotiable.
  • Physical goods (groceries, clothing, tickets) can use external payment providers, but you can't directly promote external payment links within the app UI.
  • Regional rules vary significantly. The EU, China, South Korea, and Japan have different requirements. Build flexibility into your payment architecture.
  • Read the current app store guidelines before you start building payment flows, not after. Both platforms update regularly.
  • Always validate receipts server-side for in-app purchases. Never trust the client.
  • Test payment flows thoroughly before submission, including error cases, refunds, and subscription lifecycle events.
  • Keep documentation of which payment method applies to which product type in which region. This pays off during review and during scaling.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles