Back to Blog

E-commerce App Development 2026: Complete Guide to Building a Shopping App

Build a high-converting e-commerce mobile app in 2026. Covers React Native/Flutter code examples, Shopify integration, payment processing, cart optimization, and development costs ($30K-150K).

Hevcode Team
January 5, 2026

Mobile commerce will hit $620 billion in the US alone by 2026. Apps convert 3x better than mobile web, and users spend 3x more per session. This guide shows you how to build an e-commerce app that converts — with code examples, cost breakdowns, and proven strategies.

Why E-commerce Apps Outperform Mobile Web

Metric Mobile Web Native App Improvement
Conversion rate 1.8% 5.4% 3x higher
Average order value $85 $127 49% higher
Session duration 2.5 min 7.2 min 188% longer
30-day retention 8% 28% 3.5x better
Push notification open rate N/A 90% vs 20% email

Business Case

For a store with $1M annual mobile revenue:

  • Mobile web conversion: 1.8% = $1M
  • App conversion: 5.4% = $3M potential
  • ROI: App development ($50-150K) pays back in 2-4 months

Essential Features for E-commerce Apps

Product Discovery

// React Native product listing with infinite scroll
import React, { useState, useCallback } from 'react';
import { FlatList, Image, Text, View, TouchableOpacity, ActivityIndicator } from 'react-native';

const ProductList = ({ category }) => {
  const [products, setProducts] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const fetchProducts = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    try {
      const response = await fetch(
        `${API_URL}/products?category=${category}&page=${page}&limit=20`
      );
      const data = await response.json();

      if (data.products.length < 20) {
        setHasMore(false);
      }

      setProducts(prev => [...prev, ...data.products]);
      setPage(prev => prev + 1);
    } catch (error) {
      console.error('Failed to fetch products:', error);
    } finally {
      setLoading(false);
    }
  }, [page, category, loading, hasMore]);

  const renderProduct = ({ item }) => (
    <TouchableOpacity
      style={styles.productCard}
      onPress={() => navigation.navigate('Product', { id: item.id })}
    >
      <Image
        source={{ uri: item.images[0] }}
        style={styles.productImage}
        resizeMode="cover"
      />
      {item.salePrice && (
        <View style={styles.saleBadge}>
          <Text style={styles.saleText}>
            {Math.round((1 - item.salePrice / item.price) * 100)}% OFF
          </Text>
        </View>
      )}
      <View style={styles.productInfo}>
        <Text style={styles.productName} numberOfLines={2}>
          {item.name}
        </Text>
        <View style={styles.priceRow}>
          <Text style={styles.price}>
            ${item.salePrice || item.price}
          </Text>
          {item.salePrice && (
            <Text style={styles.originalPrice}>${item.price}</Text>
          )}
        </View>
        <View style={styles.ratingRow}>
          <Text>⭐ {item.rating}</Text>
          <Text style={styles.reviewCount}>({item.reviewCount})</Text>
        </View>
      </View>
    </TouchableOpacity>
  );

  return (
    <FlatList
      data={products}
      renderItem={renderProduct}
      keyExtractor={item => item.id}
      numColumns={2}
      onEndReached={fetchProducts}
      onEndReachedThreshold={0.5}
      ListFooterComponent={loading ? <ActivityIndicator /> : null}
    />
  );
};

Smart Search with Autocomplete

// Search with debounce and suggestions
import { useState, useEffect, useCallback } from 'react';
import { TextInput, FlatList, View, Text, TouchableOpacity } from 'react-native';
import debounce from 'lodash/debounce';

const ProductSearch = ({ onProductSelect }) => {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);
  const [recentSearches, setRecentSearches] = useState([]);

  // Debounced search to reduce API calls
  const searchProducts = useCallback(
    debounce(async (searchQuery) => {
      if (searchQuery.length < 2) {
        setSuggestions([]);
        return;
      }

      try {
        const response = await fetch(
          `${API_URL}/search/suggestions?q=${encodeURIComponent(searchQuery)}`
        );
        const data = await response.json();
        setSuggestions(data.suggestions);
      } catch (error) {
        console.error('Search error:', error);
      }
    }, 300),
    []
  );

  useEffect(() => {
    searchProducts(query);
  }, [query]);

  const handleSelect = async (item) => {
    // Save to recent searches
    const updated = [item, ...recentSearches.filter(r => r.id !== item.id)].slice(0, 5);
    setRecentSearches(updated);
    await AsyncStorage.setItem('recentSearches', JSON.stringify(updated));

    onProductSelect(item);
  };

  return (
    <View style={styles.searchContainer}>
      <View style={styles.searchInputWrapper}>
        <SearchIcon />
        <TextInput
          style={styles.searchInput}
          placeholder="Search products..."
          value={query}
          onChangeText={setQuery}
          returnKeyType="search"
        />
        {query.length > 0 && (
          <TouchableOpacity onPress={() => setQuery('')}>
            <ClearIcon />
          </TouchableOpacity>
        )}
      </View>

      {query.length === 0 && recentSearches.length > 0 && (
        <View style={styles.recentSection}>
          <Text style={styles.sectionTitle}>Recent Searches</Text>
          {recentSearches.map(item => (
            <TouchableOpacity
              key={item.id}
              onPress={() => handleSelect(item)}
              style={styles.recentItem}
            >
              <ClockIcon />
              <Text>{item.name}</Text>
            </TouchableOpacity>
          ))}
        </View>
      )}

      {suggestions.length > 0 && (
        <FlatList
          data={suggestions}
          keyExtractor={item => item.id}
          renderItem={({ item }) => (
            <TouchableOpacity
              style={styles.suggestionItem}
              onPress={() => handleSelect(item)}
            >
              <Image source={{ uri: item.thumbnail }} style={styles.suggestionImage} />
              <View>
                <Text style={styles.suggestionName}>{item.name}</Text>
                <Text style={styles.suggestionPrice}>${item.price}</Text>
              </View>
            </TouchableOpacity>
          )}
        />
      )}
    </View>
  );
};

Shopping Cart with Persistence

// Cart context with local storage persistence
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

const CartContext = createContext();

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingIndex = state.items.findIndex(
        item => item.id === action.payload.id &&
                item.variant === action.payload.variant
      );

      if (existingIndex >= 0) {
        const newItems = [...state.items];
        newItems[existingIndex].quantity += action.payload.quantity;
        return { ...state, items: newItems };
      }

      return {
        ...state,
        items: [...state.items, action.payload],
      };
    }

    case 'UPDATE_QUANTITY': {
      if (action.payload.quantity <= 0) {
        return {
          ...state,
          items: state.items.filter(item => item.id !== action.payload.id),
        };
      }

      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        ),
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload.id),
      };

    case 'APPLY_COUPON':
      return { ...state, coupon: action.payload };

    case 'CLEAR_CART':
      return { items: [], coupon: null };

    case 'LOAD_CART':
      return action.payload;

    default:
      return state;
  }
};

export const CartProvider = ({ children }) => {
  const [state, dispatch] = useReducer(cartReducer, { items: [], coupon: null });

  // Load cart from storage on mount
  useEffect(() => {
    const loadCart = async () => {
      try {
        const savedCart = await AsyncStorage.getItem('cart');
        if (savedCart) {
          dispatch({ type: 'LOAD_CART', payload: JSON.parse(savedCart) });
        }
      } catch (error) {
        console.error('Failed to load cart:', error);
      }
    };
    loadCart();
  }, []);

  // Persist cart changes
  useEffect(() => {
    AsyncStorage.setItem('cart', JSON.stringify(state));
  }, [state]);

  // Calculate totals
  const subtotal = state.items.reduce(
    (sum, item) => sum + (item.salePrice || item.price) * item.quantity,
    0
  );

  const discount = state.coupon
    ? state.coupon.type === 'percentage'
      ? subtotal * (state.coupon.value / 100)
      : state.coupon.value
    : 0;

  const total = subtotal - discount;
  const itemCount = state.items.reduce((sum, item) => sum + item.quantity, 0);

  return (
    <CartContext.Provider
      value={{
        items: state.items,
        coupon: state.coupon,
        subtotal,
        discount,
        total,
        itemCount,
        dispatch,
      }}
    >
      {children}
    </CartContext.Provider>
  );
};

export const useCart = () => useContext(CartContext);

Checkout with Stripe

// Checkout screen with Stripe payment
import React, { useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { CardField, useStripe } from '@stripe/stripe-react-native';
import { useCart } from '../context/CartContext';

const CheckoutScreen = ({ navigation }) => {
  const { items, subtotal, discount, total, dispatch } = useCart();
  const { confirmPayment } = useStripe();
  const [selectedAddress, setSelectedAddress] = useState(null);
  const [selectedShipping, setSelectedShipping] = useState(null);
  const [loading, setLoading] = useState(false);

  const shippingOptions = [
    { id: 'standard', name: 'Standard Shipping', price: 5.99, days: '5-7 business days' },
    { id: 'express', name: 'Express Shipping', price: 12.99, days: '2-3 business days' },
    { id: 'overnight', name: 'Overnight Shipping', price: 24.99, days: 'Next business day' },
  ];

  const handleCheckout = async () => {
    if (!selectedAddress) {
      Alert.alert('Error', 'Please select a shipping address');
      return;
    }
    if (!selectedShipping) {
      Alert.alert('Error', 'Please select a shipping method');
      return;
    }

    setLoading(true);

    try {
      // Step 1: Create order and get payment intent from backend
      const orderResponse = await fetch(`${API_URL}/orders`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${userToken}`,
        },
        body: JSON.stringify({
          items: items.map(item => ({
            productId: item.id,
            variantId: item.variantId,
            quantity: item.quantity,
          })),
          shippingAddressId: selectedAddress.id,
          shippingMethod: selectedShipping.id,
          couponCode: cart.coupon?.code,
        }),
      });

      const { orderId, clientSecret, orderTotal } = await orderResponse.json();

      // Step 2: Confirm payment with Stripe
      const { error, paymentIntent } = await confirmPayment(clientSecret, {
        paymentMethodType: 'Card',
        paymentMethodData: {
          billingDetails: {
            name: selectedAddress.name,
            email: user.email,
            address: {
              line1: selectedAddress.line1,
              city: selectedAddress.city,
              state: selectedAddress.state,
              postalCode: selectedAddress.postalCode,
              country: selectedAddress.country,
            },
          },
        },
      });

      if (error) {
        Alert.alert('Payment Failed', error.message);
        // Update order status to failed
        await fetch(`${API_URL}/orders/${orderId}/status`, {
          method: 'PATCH',
          headers: { 'Authorization': `Bearer ${userToken}` },
          body: JSON.stringify({ status: 'payment_failed' }),
        });
      } else if (paymentIntent.status === 'Succeeded') {
        // Clear cart and navigate to success
        dispatch({ type: 'CLEAR_CART' });
        navigation.replace('OrderConfirmation', { orderId });
      }
    } catch (error) {
      Alert.alert('Error', 'Something went wrong. Please try again.');
      console.error('Checkout error:', error);
    } finally {
      setLoading(false);
    }
  };

  const grandTotal = total + (selectedShipping?.price || 0);

  return (
    <ScrollView style={styles.container}>
      {/* Address Selection */}
      <AddressSelector
        selected={selectedAddress}
        onSelect={setSelectedAddress}
      />

      {/* Shipping Options */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Shipping Method</Text>
        {shippingOptions.map(option => (
          <TouchableOpacity
            key={option.id}
            style={[
              styles.shippingOption,
              selectedShipping?.id === option.id && styles.selectedOption,
            ]}
            onPress={() => setSelectedShipping(option)}
          >
            <View>
              <Text style={styles.shippingName}>{option.name}</Text>
              <Text style={styles.shippingDays}>{option.days}</Text>
            </View>
            <Text style={styles.shippingPrice}>${option.price}</Text>
          </TouchableOpacity>
        ))}
      </View>

      {/* Order Summary */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Order Summary</Text>
        {items.map(item => (
          <View key={item.id} style={styles.orderItem}>
            <Text>{item.name} x {item.quantity}</Text>
            <Text>${((item.salePrice || item.price) * item.quantity).toFixed(2)}</Text>
          </View>
        ))}
        <View style={styles.divider} />
        <View style={styles.totalRow}>
          <Text>Subtotal</Text>
          <Text>${subtotal.toFixed(2)}</Text>
        </View>
        {discount > 0 && (
          <View style={styles.totalRow}>
            <Text style={styles.discountText}>Discount</Text>
            <Text style={styles.discountText}>-${discount.toFixed(2)}</Text>
          </View>
        )}
        <View style={styles.totalRow}>
          <Text>Shipping</Text>
          <Text>${(selectedShipping?.price || 0).toFixed(2)}</Text>
        </View>
        <View style={[styles.totalRow, styles.grandTotal]}>
          <Text style={styles.grandTotalText}>Total</Text>
          <Text style={styles.grandTotalText}>${grandTotal.toFixed(2)}</Text>
        </View>
      </View>

      {/* Payment */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Payment</Text>
        <CardField
          postalCodeEnabled={true}
          placeholders={{ number: '4242 4242 4242 4242' }}
          style={styles.cardField}
        />
      </View>

      <TouchableOpacity
        style={[styles.checkoutButton, loading && styles.disabledButton]}
        onPress={handleCheckout}
        disabled={loading}
      >
        <Text style={styles.checkoutButtonText}>
          {loading ? 'Processing...' : `Pay $${grandTotal.toFixed(2)}`}
        </Text>
      </TouchableOpacity>
    </ScrollView>
  );
};

Express Checkout (Apple Pay / Google Pay)

// Express checkout component
import { ApplePayButton, GooglePayButton, useApplePay, useGooglePay } from '@stripe/stripe-react-native';
import { Platform } from 'react-native';

const ExpressCheckout = ({ total, onSuccess }) => {
  const { presentApplePay, confirmApplePayPayment } = useApplePay();
  const { presentGooglePay, confirmGooglePayPayment } = useGooglePay();

  const handleApplePay = async () => {
    const { error, paymentMethod } = await presentApplePay({
      cartItems: [
        { label: 'Subtotal', amount: total.subtotal.toString() },
        { label: 'Shipping', amount: total.shipping.toString() },
        { label: 'Your Store', amount: total.grand.toString(), paymentType: 'Immediate' },
      ],
      country: 'US',
      currency: 'USD',
      requiredShippingAddressFields: ['postalAddress', 'phoneNumber', 'emailAddress'],
      requiredBillingContactFields: ['phoneNumber', 'name'],
    });

    if (error) {
      Alert.alert('Apple Pay Error', error.message);
      return;
    }

    // Get payment intent from backend
    const { clientSecret } = await createPaymentIntent(total.grand);

    const { error: confirmError } = await confirmApplePayPayment(clientSecret);

    if (confirmError) {
      Alert.alert('Payment Failed', confirmError.message);
    } else {
      onSuccess();
    }
  };

  const handleGooglePay = async () => {
    const { error } = await presentGooglePay({
      testEnv: __DEV__,
      merchantName: 'Your Store',
      countryCode: 'US',
      currencyCode: 'USD',
      amount: total.grand,
      billingAddressRequired: true,
    });

    if (error) {
      Alert.alert('Google Pay Error', error.message);
      return;
    }

    const { clientSecret } = await createPaymentIntent(total.grand);
    const { error: confirmError } = await confirmGooglePayPayment(clientSecret);

    if (confirmError) {
      Alert.alert('Payment Failed', confirmError.message);
    } else {
      onSuccess();
    }
  };

  return (
    <View style={styles.expressCheckout}>
      {Platform.OS === 'ios' ? (
        <ApplePayButton
          onPress={handleApplePay}
          type="buy"
          style={styles.applePayButton}
        />
      ) : (
        <GooglePayButton
          onPress={handleGooglePay}
          type="buy"
          style={styles.googlePayButton}
        />
      )}
    </View>
  );
};

Shopify Integration (Headless Commerce)

// Shopify Storefront API client
import Client from 'shopify-buy';

const shopifyClient = Client.buildClient({
  domain: 'your-store.myshopify.com',
  storefrontAccessToken: process.env.SHOPIFY_STOREFRONT_TOKEN,
});

class ShopifyService {
  // Fetch products with pagination
  async getProducts({ first = 20, after = null, query = '' }) {
    const products = await shopifyClient.product.fetchQuery({
      first,
      after,
      query,
    });

    return {
      products: products.map(this.formatProduct),
      hasNextPage: products.length === first,
      lastCursor: products[products.length - 1]?.id,
    };
  }

  // Get single product with variants
  async getProduct(handle) {
    const product = await shopifyClient.product.fetchByHandle(handle);

    return {
      id: product.id,
      title: product.title,
      description: product.descriptionHtml,
      images: product.images.map(img => img.src),
      variants: product.variants.map(variant => ({
        id: variant.id,
        title: variant.title,
        price: variant.price.amount,
        compareAtPrice: variant.compareAtPrice?.amount,
        available: variant.available,
        options: variant.selectedOptions,
      })),
      options: product.options.map(opt => ({
        name: opt.name,
        values: opt.values,
      })),
    };
  }

  // Create checkout
  async createCheckout(lineItems) {
    const checkout = await shopifyClient.checkout.create({
      lineItems: lineItems.map(item => ({
        variantId: item.variantId,
        quantity: item.quantity,
      })),
    });

    return {
      checkoutId: checkout.id,
      webUrl: checkout.webUrl, // For web checkout fallback
      subtotal: checkout.subtotalPrice.amount,
      total: checkout.totalPrice.amount,
    };
  }

  // Add discount code
  async applyDiscount(checkoutId, discountCode) {
    const checkout = await shopifyClient.checkout.addDiscount(
      checkoutId,
      discountCode
    );

    return {
      applied: checkout.discountApplications.length > 0,
      discount: checkout.discountApplications[0]?.value,
      newTotal: checkout.totalPrice.amount,
    };
  }

  // Update shipping address
  async updateShippingAddress(checkoutId, address) {
    const checkout = await shopifyClient.checkout.updateShippingAddress(
      checkoutId,
      {
        address1: address.line1,
        address2: address.line2,
        city: address.city,
        province: address.state,
        zip: address.postalCode,
        country: address.country,
        firstName: address.firstName,
        lastName: address.lastName,
        phone: address.phone,
      }
    );

    return {
      availableShippingRates: checkout.availableShippingRates?.shippingRates,
    };
  }

  formatProduct(product) {
    return {
      id: product.id,
      handle: product.handle,
      title: product.title,
      description: product.description,
      image: product.images[0]?.src,
      price: product.variants[0]?.price.amount,
      compareAtPrice: product.variants[0]?.compareAtPrice?.amount,
      available: product.availableForSale,
    };
  }
}

export const shopifyService = new ShopifyService();

Flutter Implementation

// Flutter e-commerce cart with provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CartItem {
  final String id;
  final String productId;
  final String name;
  final String image;
  final double price;
  final double? salePrice;
  final int quantity;
  final String? variant;

  CartItem({
    required this.id,
    required this.productId,
    required this.name,
    required this.image,
    required this.price,
    this.salePrice,
    required this.quantity,
    this.variant,
  });

  double get effectivePrice => salePrice ?? price;
  double get total => effectivePrice * quantity;
}

class CartProvider extends ChangeNotifier {
  List<CartItem> _items = [];
  String? _couponCode;
  double _discountAmount = 0;

  List<CartItem> get items => _items;
  int get itemCount => _items.fold(0, (sum, item) => sum + item.quantity);
  double get subtotal => _items.fold(0, (sum, item) => sum + item.total);
  double get discount => _discountAmount;
  double get total => subtotal - _discountAmount;

  void addItem(CartItem item) {
    final existingIndex = _items.indexWhere(
      (i) => i.productId == item.productId && i.variant == item.variant,
    );

    if (existingIndex >= 0) {
      _items[existingIndex] = CartItem(
        id: _items[existingIndex].id,
        productId: item.productId,
        name: item.name,
        image: item.image,
        price: item.price,
        salePrice: item.salePrice,
        quantity: _items[existingIndex].quantity + item.quantity,
        variant: item.variant,
      );
    } else {
      _items.add(item);
    }
    notifyListeners();
    _saveCart();
  }

  void updateQuantity(String itemId, int quantity) {
    if (quantity <= 0) {
      removeItem(itemId);
      return;
    }

    final index = _items.indexWhere((i) => i.id == itemId);
    if (index >= 0) {
      _items[index] = CartItem(
        id: _items[index].id,
        productId: _items[index].productId,
        name: _items[index].name,
        image: _items[index].image,
        price: _items[index].price,
        salePrice: _items[index].salePrice,
        quantity: quantity,
        variant: _items[index].variant,
      );
      notifyListeners();
      _saveCart();
    }
  }

  void removeItem(String itemId) {
    _items.removeWhere((i) => i.id == itemId);
    notifyListeners();
    _saveCart();
  }

  Future<bool> applyCoupon(String code) async {
    try {
      final response = await http.post(
        Uri.parse('$apiUrl/coupons/validate'),
        body: jsonEncode({'code': code, 'subtotal': subtotal}),
        headers: {'Content-Type': 'application/json'},
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        _couponCode = code;
        _discountAmount = data['discountAmount'].toDouble();
        notifyListeners();
        return true;
      }
      return false;
    } catch (e) {
      return false;
    }
  }

  void clearCart() {
    _items = [];
    _couponCode = null;
    _discountAmount = 0;
    notifyListeners();
    _saveCart();
  }

  Future<void> _saveCart() async {
    final prefs = await SharedPreferences.getInstance();
    final cartJson = jsonEncode(_items.map((i) => {
      'id': i.id,
      'productId': i.productId,
      'name': i.name,
      'image': i.image,
      'price': i.price,
      'salePrice': i.salePrice,
      'quantity': i.quantity,
      'variant': i.variant,
    }).toList());
    await prefs.setString('cart', cartJson);
  }
}

// Cart screen widget
class CartScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CartProvider>(
      builder: (context, cart, child) {
        if (cart.items.isEmpty) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.shopping_cart_outlined, size: 80, color: Colors.grey),
                SizedBox(height: 16),
                Text('Your cart is empty', style: TextStyle(fontSize: 18)),
                SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () => Navigator.pushNamed(context, '/shop'),
                  child: Text('Start Shopping'),
                ),
              ],
            ),
          );
        }

        return Column(
          children: [
            Expanded(
              child: ListView.builder(
                itemCount: cart.items.length,
                itemBuilder: (context, index) {
                  final item = cart.items[index];
                  return CartItemCard(
                    item: item,
                    onUpdateQuantity: (qty) => cart.updateQuantity(item.id, qty),
                    onRemove: () => cart.removeItem(item.id),
                  );
                },
              ),
            ),
            CartSummary(cart: cart),
          ],
        );
      },
    );
  }
}

Push Notifications for Engagement

// Backend push notification service
const admin = require('firebase-admin');
const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

class NotificationService {
  // Abandoned cart reminder (send after 1 hour)
  async sendAbandonedCartReminder(userId) {
    const user = await prisma.user.findUnique({
      where: { id: userId },
      include: {
        cart: { include: { items: true } },
        pushTokens: true,
      },
    });

    if (!user.cart?.items.length || !user.pushTokens.length) return;

    const cartTotal = user.cart.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );

    const message = {
      notification: {
        title: 'You left something behind! šŸ›’',
        body: `Complete your order of $${cartTotal.toFixed(2)} before items sell out`,
      },
      data: {
        type: 'abandoned_cart',
        cartId: user.cart.id,
        deepLink: 'yourapp://cart',
      },
      tokens: user.pushTokens.map(t => t.token),
    };

    await admin.messaging().sendEachForMulticast(message);

    await prisma.notification.create({
      data: {
        userId,
        type: 'abandoned_cart',
        title: message.notification.title,
        body: message.notification.body,
      },
    });
  }

  // Price drop alert
  async sendPriceDropAlert(productId, newPrice, oldPrice) {
    const wishlists = await prisma.wishlistItem.findMany({
      where: { productId },
      include: {
        wishlist: {
          include: {
            user: { include: { pushTokens: true } },
          },
        },
      },
    });

    const discount = Math.round((1 - newPrice / oldPrice) * 100);

    for (const item of wishlists) {
      const user = item.wishlist.user;
      if (!user.pushTokens.length) continue;

      await admin.messaging().sendEachForMulticast({
        notification: {
          title: `Price Drop Alert! šŸ”„ ${discount}% OFF`,
          body: `${item.productName} is now $${newPrice} (was $${oldPrice})`,
        },
        data: {
          type: 'price_drop',
          productId,
          deepLink: `yourapp://product/${productId}`,
        },
        tokens: user.pushTokens.map(t => t.token),
      });
    }
  }

  // Back in stock notification
  async sendBackInStockAlert(productId, productName) {
    const subscribers = await prisma.stockAlert.findMany({
      where: { productId, notified: false },
      include: {
        user: { include: { pushTokens: true } },
      },
    });

    for (const subscriber of subscribers) {
      if (!subscriber.user.pushTokens.length) continue;

      await admin.messaging().sendEachForMulticast({
        notification: {
          title: 'Back in Stock! šŸŽ‰',
          body: `${productName} is available again. Get it before it sells out!`,
        },
        data: {
          type: 'back_in_stock',
          productId,
          deepLink: `yourapp://product/${productId}`,
        },
        tokens: subscriber.user.pushTokens.map(t => t.token),
      });

      await prisma.stockAlert.update({
        where: { id: subscriber.id },
        data: { notified: true },
      });
    }
  }
}

Development Cost Breakdown

MVP E-commerce App

Feature Cost Range Timeline
Product catalog & search $8,000 - 15,000 2-3 weeks
User accounts & auth $5,000 - 10,000 1-2 weeks
Shopping cart $6,000 - 12,000 2 weeks
Checkout & payments $10,000 - 18,000 2-3 weeks
Order management $8,000 - 15,000 2 weeks
Push notifications $4,000 - 8,000 1 week
Admin panel $10,000 - 20,000 2-3 weeks
UI/UX design $8,000 - 15,000 2-3 weeks
MVP Total $60,000 - 115,000 3-4 months

Full-Featured App

Additional Features Cost Range
Wishlist & favorites $4,000 - 8,000
Reviews & ratings $6,000 - 12,000
Loyalty program $10,000 - 20,000
AR try-on $20,000 - 40,000
Advanced personalization $15,000 - 30,000
Multi-vendor marketplace $25,000 - 50,000
Full App Total $140,000 - 275,000

Headless Commerce (Shopify/BigCommerce)

Component Cost Range
Mobile app development $40,000 - 80,000
Shopify Plus subscription $2,300/month
Custom integrations $10,000 - 25,000
Total First Year $80,000 - 135,000

Key Performance Benchmarks

Metric Target Industry Average
App load time < 2 seconds 3.5 seconds
Add-to-cart rate > 10% 8%
Cart abandonment < 70% 75%
Checkout completion > 35% 25%
30-day retention > 25% 18%
Push notification opt-in > 50% 40%

Frequently Asked Questions

How much does it cost to build an e-commerce app?

An MVP e-commerce app costs $60,000-115,000 and takes 3-4 months. Full-featured apps with loyalty programs, AR features, and marketplace functionality cost $140,000-275,000. Using headless commerce (Shopify) reduces mobile development costs to $40,000-80,000.

Should I use Shopify or build a custom backend?

Use Shopify if: you need to launch fast, have under $10M annual revenue, want proven infrastructure. Build custom if: you need unique features, have complex business logic, want to avoid platform fees at scale. Many businesses start with Shopify and migrate later.

How long does it take to build an e-commerce app?

MVP: 3-4 months. Full-featured app: 6-9 months. With headless commerce (existing backend): 2-3 months for mobile app only. Timeline depends heavily on feature complexity and integration requirements.

What's the best tech stack for e-commerce apps?

Mobile: React Native (fastest development, largest talent pool) or Flutter (better performance, growing fast). Backend: Node.js with PostgreSQL, or headless commerce (Shopify, BigCommerce). Payments: Stripe (best developer experience) or Adyen (better for enterprise).

How do I reduce cart abandonment in my app?

Key strategies: (1) Enable guest checkout — 35% abandon due to account requirements, (2) Add express checkout (Apple/Google Pay), (3) Send push notifications within 1 hour of abandonment, (4) Show clear shipping costs early, (5) Simplify checkout to 3 steps or fewer.

Ready to Build Your E-commerce App?

E-commerce apps deliver 3x higher conversion than mobile web. Whether you're launching a new store or adding mobile to existing operations, the investment pays back quickly.

We've built e-commerce apps for fashion, electronics, grocery, and marketplace clients — all with optimized checkout flows that convert.

Get in touch:


Related Articles

Tags:E-commerceMobile CommerceShopping AppShopifyReact NativeFlutterStripe

Need help with your project?

We've helped 534+ clients build successful apps. Let's discuss yours.

Ready to Build Your App?

534+ projects delivered • 4.9ā˜… rating • 6+ years experience

Let's discuss your project — no obligations, just a straightforward conversation.