Skip to content

📱 Mobile App with Payments

Build a React Native mobile app with authentication, content, and in-app purchases.

📖 Cross-Ecosystem Example

This is a complete tutorial demonstrating the standard Flowfull architecture pattern using flowfull-node for your custom mobile backend.

🎯 What You'll Build

A production-ready mobile application with:

  • ✅ Social login (Google, Apple)
  • ✅ Offline support with local storage
  • ✅ Push notifications
  • ✅ In-app purchases (Apple Pay / Google Pay)
  • ✅ Subscription management
  • ✅ Content paywall
  • ✅ User profiles with image upload

Time to complete: ~120 minutes Platforms: iOS & Android


🛠️ Tech Stack

ComponentTechnologyPurpose
🔐 FlowlessManaged Service (pubflow.com)Authentication - Social login (Google, Apple), sessions, profiles
FlowfullNode.js (flowfull-node)Your custom backend - Content API, user data, purchases
🎨 Flowfull ClientsReact Native (@pubflow/react-native)Your mobile app - Expo with offline support
💳 Bridge PaymentsManaged ServicePayment processing - In-app purchases, subscriptions

🏗️ Standard Architecture Pattern

  • Flowless = Authentication backend (managed, no code needed)
  • Flowfull = Your custom backend using flowfull-node (you write this)
  • Flowfull Clients = Your mobile app using @pubflow/react-native (you write this)
  • Bridge Payments = Payment backend (managed, API integration only)

🏗️ Architecture

Flow:

  1. Mobile App (React Native) → Your Backend (Flowfull/flowfull-node)
  2. Your Backend → Flowless for social authentication
  3. Your Backend → Bridge Payments for in-app purchases
  4. Bridge Payments → Native payment providers (Apple Pay / Google Pay)

📋 Prerequisites

  • Bun v1.0+ (Install) or Node.js 18+
  • Database - PostgreSQL, MySQL, or LibSQL/Turso
  • Pubflow account - Sign up at pubflow.com
  • Expo CLI - Install with npm install -g expo-cli
  • iOS Simulator or Android Emulator
  • Apple Developer account (for iOS in-app purchases)
  • Google Play Console account (for Android in-app purchases)
  • ✅ Basic knowledge of React Native and TypeScript

🚀 Step 1: Setup Flowless & Backend (15 min)

1.1 Create Flowless Instance

  1. Visit pubflow.com
  2. Sign up or log in
  3. Click "Create Flowless Instance"
  4. Name it: mobile-auth
  5. Configure OAuth (Google, Apple) for social login
  6. Copy your Bridge Secret

Your Flowless URL will be:

https://mobile-auth.pubflow.com

📖 Learn More

For detailed Flowless setup, see Flowless Getting Started

1.2 Setup Flowfull Backend

📦 Official Starter Kit

flowfull-node is the official, production-ready starter kit maintained by the Pubflow team.

View Documentation →

bash
# Clone the official flowfull-node template
git clone https://github.com/pubflow/flowfull-node.git mobile-backend
cd mobile-backend

# Install dependencies
bun install
# or: npm install

# Configure environment
cp .env.example .env

Edit .env:

env
# Flowless Configuration
FLOWLESS_API_URL=https://mobile-auth.pubflow.com
BRIDGE_VALIDATION_SECRET=your_bridge_secret_here

# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/mobile

# Server
PORT=3001
NODE_ENV=development

# Optional: Cache
REDIS_URL=redis://localhost:6379

📱 Step 2: Setup React Native App (15 min)

2.1 Clone create-pubflow-rn (Official Starter Kit)

📱 Official Mobile Starter

create-pubflow-rn is the official React Native Expo starter kit with complete authentication, offline support, and native components.

View Documentation →

bash
# Clone the official React Native Expo starter kit
git clone https://github.com/pubflow/create-pubflow-rn.git my-app
cd my-app

# Install dependencies
npm install
# or: bun install

2.2 Configure Environment

Copy the example environment file:

bash
cp .env.example .env

Edit .env:

env
# API Configuration
EXPO_PUBLIC_BRIDGE_URL=http://localhost:3001
EXPO_PUBLIC_API_BASE_URL=http://localhost:3001

# Branding (Optional)
EXPO_PUBLIC_APP_NAME=My App
EXPO_PUBLIC_PRIMARY_COLOR=#006aff

🔧 Environment Variables

  • EXPO_PUBLIC_BRIDGE_URL: Your Flowfull backend URL from Step 1.2
  • EXPO_PUBLIC_API_BASE_URL: Same as BRIDGE_URL for API calls

1.3 Install Dependencies

bash
# Payment processing
npx expo install expo-apple-authentication
npx expo install @react-native-google-signin/google-signin

# Push notifications
npx expo install expo-notifications

# Image picker
npx expo install expo-image-picker

🔐 Step 2: Social Authentication (20 min)

2.1 Login Screen

tsx
// app/login.tsx
import { useState } from 'react'
import { View, Text, TouchableOpacity } from 'react-native'
import { useAuth } from '@pubflow/react-native'
import * as AppleAuthentication from 'expo-apple-authentication'
import { GoogleSignin } from '@react-native-google-signin/google-signin'

export default function LoginScreen() {
  const { login, loginWithOAuth } = useAuth()
  const [loading, setLoading] = useState(false)
  
  const handleAppleLogin = async () => {
    try {
      const credential = await AppleAuthentication.signInAsync({
        requestedScopes: [
          AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
          AppleAuthentication.AppleAuthenticationScope.EMAIL,
        ],
      })
      
      await loginWithOAuth('apple', {
        id_token: credential.identityToken,
        user: credential.user
      })
    } catch (error) {
      console.error('Apple login failed:', error)
    }
  }
  
  const handleGoogleLogin = async () => {
    try {
      await GoogleSignin.hasPlayServices()
      const userInfo = await GoogleSignin.signIn()
      
      await loginWithOAuth('google', {
        id_token: userInfo.idToken
      })
    } catch (error) {
      console.error('Google login failed:', error)
    }
  }
  
  return (
    <View className="flex-1 justify-center items-center p-6 bg-white">
      <Text className="text-3xl font-bold mb-8">Welcome</Text>
      
      <AppleAuthentication.AppleAuthenticationButton
        buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
        buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
        cornerRadius={8}
        style={{ width: 280, height: 50, marginBottom: 16 }}
        onPress={handleAppleLogin}
      />
      
      <TouchableOpacity
        onPress={handleGoogleLogin}
        className="bg-white border border-gray-300 rounded-lg px-6 py-3 w-70 mb-4"
      >
        <Text className="text-center font-semibold">Continue with Google</Text>
      </TouchableOpacity>
    </View>
  )
}

📱 Step 3: Content Feed (25 min)

3.1 Content API

typescript
// backend/src/routes/content.ts
import { Hono } from 'hono'
import { validateSession } from '../middleware/auth'

const content = new Hono()

content.get('/', async (c) => {
  const sessionId = c.req.header('X-Session-Id')
  let isPremium = false
  
  if (sessionId) {
    const user = await validateUserSession(sessionId)
    isPremium = user?.is_premium || false
  }
  
  const posts = await db
    .selectFrom('posts')
    .selectAll()
    .where('published', '=', true)
    .where((eb) => 
      isPremium 
        ? eb.or([eb('premium_only', '=', false), eb('premium_only', '=', true)])
        : eb('premium_only', '=', false)
    )
    .execute()
  
  return c.json({ posts, is_premium: isPremium })
})

export default content

3.2 Feed Screen

tsx
// app/(tabs)/feed.tsx
import { FlatList, View, Text, Image, TouchableOpacity } from 'react-native'
import { useBridge, useAuth } from '@pubflow/react-native'
import { router } from 'expo-router'

export default function FeedScreen() {
  const { user } = useAuth()
  const { data: content } = useBridge('/content')
  
  const renderPost = ({ item }: any) => (
    <TouchableOpacity
      onPress={() => router.push(`/post/${item.id}`)}
      className="bg-white rounded-lg mb-4 overflow-hidden shadow"
    >
      {item.cover_image && (
        <Image
          source={{ uri: item.cover_image }}
          className="w-full h-48"
          resizeMode="cover"
        />
      )}
      
      <View className="p-4">
        <Text className="text-xl font-bold mb-2">{item.title}</Text>
        <Text className="text-gray-600 mb-3">{item.excerpt}</Text>
        
        {item.premium_only && !user?.is_premium && (
          <View className="bg-yellow-100 px-3 py-2 rounded">
            <Text className="text-yellow-800 text-sm font-semibold">
              🔒 Premium Content
            </Text>
          </View>
        )}
      </View>
    </TouchableOpacity>
  )
  
  return (
    <View className="flex-1 bg-gray-50">
      <FlatList
        data={content?.posts || []}
        renderItem={renderPost}
        keyExtractor={(item) => item.id}
        contentContainerStyle={{ padding: 16 }}
      />
    </View>
  )
}

💳 Step 4: In-App Purchases (30 min)

4.1 Premium Screen

tsx
// app/(tabs)/premium.tsx
import { View, Text, TouchableOpacity, Alert } from 'react-native'
import { useAuth } from '@pubflow/react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'

export default function PremiumScreen() {
  const { user } = useAuth()
  
  const handleSubscribe = async () => {
    try {
      const sessionId = await AsyncStorage.getItem('pubflow_session_id')
      
      // Create payment intent
      const response = await fetch(`${process.env.EXPO_PUBLIC_PAYMENTS_URL}/intents`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Session-Id': sessionId || ''
        },
        body: JSON.stringify({
          total_cents: 999, // $9.99
          currency: 'USD',
          concept: 'Premium Subscription',
          recurring: true,
          interval: 'month',
          payment_method: 'apple_pay' // or 'google_pay'
        })
      })
      
      const { client_secret } = await response.json()
      
      // Process payment with native payment sheet
      // Implementation depends on payment provider
      
      Alert.alert('Success', 'Welcome to Premium!')
    } catch (error) {
      Alert.alert('Error', 'Subscription failed')
    }
  }
  
  if (user?.is_premium) {
    return (
      <View className="flex-1 justify-center items-center p-6">
        <Text className="text-2xl font-bold mb-4">You're Premium! 🎉</Text>
        <Text className="text-gray-600 text-center">
          Enjoy unlimited access to all content
        </Text>
      </View>
    )
  }
  
  return (
    <View className="flex-1 justify-center items-center p-6 bg-white">
      <Text className="text-3xl font-bold mb-4">Go Premium</Text>
      <Text className="text-5xl font-bold mb-2">$9.99</Text>
      <Text className="text-gray-600 mb-8">per month</Text>
      
      <View className="w-full mb-8">
        <View className="flex-row items-center mb-3">
          <Text className="text-green-600 mr-2">✓</Text>
          <Text>Unlimited access to all content</Text>
        </View>
        <View className="flex-row items-center mb-3">
          <Text className="text-green-600 mr-2">✓</Text>
          <Text>Ad-free experience</Text>
        </View>
        <View className="flex-row items-center mb-3">
          <Text className="text-green-600 mr-2">✓</Text>
          <Text>Offline downloads</Text>
        </View>
        <View className="flex-row items-center mb-3">
          <Text className="text-green-600 mr-2">✓</Text>
          <Text>Early access to new features</Text>
        </View>
      </View>
      
      <TouchableOpacity
        onPress={handleSubscribe}
        className="bg-blue-600 rounded-lg px-8 py-4 w-full"
      >
        <Text className="text-white text-center font-bold text-lg">
          Subscribe Now
        </Text>
      </TouchableOpacity>
    </View>
  )
}

🔔 Step 5: Push Notifications (15 min)

tsx
// utils/notifications.ts
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'

export async function registerForPushNotifications() {
  const { status: existingStatus } = await Notifications.getPermissionsAsync()
  let finalStatus = existingStatus
  
  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync()
    finalStatus = status
  }
  
  if (finalStatus !== 'granted') {
    return null
  }
  
  const token = await Notifications.getExpoPushTokenAsync()
  
  // Send token to backend
  await fetch(`${process.env.EXPO_PUBLIC_BRIDGE_URL}/notifications/register`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token: token.data })
  })
  
  return token.data
}

🎉 Result

You've built a complete mobile app with:

  • ✅ Social authentication
  • ✅ Content feed
  • ✅ In-app purchases
  • ✅ Push notifications

📖 Next Steps

  • 📥 Offline mode - Cache content locally
  • 🎨 Dark mode - Theme support
  • 🌍 Localization - Multi-language
  • 📊 Analytics - Track user behavior