📱 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
| Component | Technology | Purpose |
|---|---|---|
| 🔐 Flowless | Managed Service (pubflow.com) | Authentication - Social login (Google, Apple), sessions, profiles |
| ⚡ Flowfull | Node.js (flowfull-node) | Your custom backend - Content API, user data, purchases |
| 🎨 Flowfull Clients | React Native (@pubflow/react-native) | Your mobile app - Expo with offline support |
| 💳 Bridge Payments | Managed Service | Payment 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:
- Mobile App (React Native) → Your Backend (Flowfull/flowfull-node)
- Your Backend → Flowless for social authentication
- Your Backend → Bridge Payments for in-app purchases
- 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
- Visit pubflow.com
- Sign up or log in
- Click "Create Flowless Instance"
- Name it:
mobile-auth - Configure OAuth (Google, Apple) for social login
- 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.
# 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 .envEdit .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.
# 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 install2.2 Configure Environment
Copy the example environment file:
cp .env.example .envEdit .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.2EXPO_PUBLIC_API_BASE_URL: Same as BRIDGE_URL for API calls
1.3 Install Dependencies
# 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
// 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
// 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 content3.2 Feed Screen
// 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
// 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)
// 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
📚 Related Resources
- 🔐 Flowless - Authentication
- ⚡ Flowfull - Backend
- 💳 Bridge Payments - Payments
- 💬 Discord - Community