Skip to main content

Complete React Native E-commerce App

This example demonstrates a complete React Native e-commerce application using the Reachu SDK, based on the official demo repository. It includes product browsing, cart management, checkout flow, and payment processing with Stripe and Klarna integration.

Project Structure

Based on the official demo repository:

ReachuDemo/
components/
Header/
CartSummaryModal/
index.js
index.js
FooterNavigation/
index.js
Screens/
Products/
components/
ProductItem/
index.js
ProductDetail/
index.js
index.js
Checkout/
index.js
Payment/
components/
StripePaymentButton/
index.js
KlarnaPaymentButton/
index.js
index.js
Container/
index.js
InitializationMain/
index.js
context/
reachu-sdk-provider.js
cartContext.js
graphql/
client.js
hooks/
product.js
cart.js
cartItem.js
checkout.js
mutations/
cart.js
cartItem.js
checkout.js
querys/
product.js
models/
productItem.js
consts/
env.js
App.tsx
package.json

Installation & Setup

1. Create New React Native Project

npx react-native init ReachuStoreApp --template react-native-template-typescript
cd ReachuStoreApp

2. Install Dependencies

Based on the demo's actual dependencies:

# Core Reachu SDK
npm install @reachu/react-native-sdk

# GraphQL
npm install @apollo/client graphql

# UI Components
npm install @rneui/base @rneui/themed
npm install react-native-paper
npm install react-native-vector-icons

# Payment Integration
npm install @stripe/stripe-react-native

# Additional utilities
npm install @react-native-community/checkbox
npm install react-native-render-html
npm install react-native-safe-area-context
npm install react-native-webview

# iOS specific
cd ios && pod install && cd ..

3. Environment Configuration

Create environment constants file:

consts/env.js
export const SELECT_CURRENCY = 'NOK';
export const SELECT_COUNTRY = 'NO';
export const API_TOKEN = 'YOUR_API_TOKEN';
export const GRAPHQL_SERVER_URL = 'https://graph-ql.reachu.io';
export const FAKE_RETURN_URL = 'https://www.example.com/confirmation.html';
export const REACHU_SERVER_URL = 'https://api.reachu.io';

Core Services

Reachu SDK Provider

context/reachu-sdk-provider.js
import React, {createContext, useContext, useMemo} from 'react';
import SdkClient from '@reachu/react-native-sdk';
import {API_TOKEN, GRAPHQL_SERVER_URL} from '../consts/env';

const TOKEN = API_TOKEN;
const ENDPOINT = GRAPHQL_SERVER_URL;

const ReachuSdkContext = createContext(null);

export const ReachuSdkProvider = ({children}) => {
const sdk = useMemo(() => new SdkClient(TOKEN, ENDPOINT), []);

return (
<ReachuSdkContext.Provider value={sdk}>
{children}
</ReachuSdkContext.Provider>
);
};

export function useReachuSdk() {
const ctx = useContext(ReachuSdkContext);
if (!ctx) {
throw new Error('useReachuSdk debe usarse dentro de ReachuSdkProvider');
}
return ctx;
}

Cart Context Provider

context/cartContext.js
import React, {createContext, useReducer, useContext} from 'react';
import {SELECT_COUNTRY, SELECT_CURRENCY} from '../consts/env';

const initialState = {
selectedCurrency: SELECT_CURRENCY,
selectedCountry: SELECT_COUNTRY,
cartId: '',
cartItems: [],
checkout: {},
selectedScreen: 'Products',
};

export const actions = {
SET_SELECTED_CURRENCY: 'SET_SELECTED_CURRENCY',
SET_SELECTED_COUNTRY: 'SET_SELECTED_COUNTRY',
SET_CART_ID: 'SET_CART_ID',
SET_SELECTED_SCREEN: 'SET_SELECTED_SCREEN',
ADD_CART_ITEM: 'ADD_CART_ITEM',
REMOVE_CART_ITEM: 'REMOVE_CART_ITEM',
UPDATE_CART_ITEM_QUANTITY: 'UPDATE_CART_ITEM_QUANTITY',
SET_CHECKOUT_STATE: 'SET_CHECKOUT_STATE',
};

const cartReducer = (state, action) => {
switch (action.type) {
case actions.SET_SELECTED_CURRENCY:
return {...state, selectedCurrency: action.payload};
case actions.SET_SELECTED_COUNTRY:
return {...state, selectedCountry: action.payload};
case actions.SET_CART_ID:
return {...state, cartId: action.payload};
case actions.SET_SELECTED_SCREEN:
return {...state, selectedScreen: action.payload};
case actions.ADD_CART_ITEM:
return {...state, cartItems: [...state.cartItems, action.payload]};
case actions.REMOVE_CART_ITEM:
return {
...state,
cartItems: state.cartItems.filter(
item => item.cartItemId !== action.payload,
),
};
case actions.UPDATE_CART_ITEM_QUANTITY:
return {
...state,
cartItems: state.cartItems.map(item =>
item.cartItemId === action.payload.cartItemId
? {...item, quantity: action.payload.quantity}
: item,
),
};
case actions.SET_CHECKOUT_STATE:
return {...state, checkout: action.payload};
default:
return state;
}
};

const CartContext = createContext();

export const CartProvider = ({children}) => {
const [state, dispatch] = useReducer(cartReducer, initialState);

return (
<CartContext.Provider value={{state, dispatch}}>
{children}
</CartContext.Provider>
);
};

export const useCart = () => {
const context = useContext(CartContext);
if (context === undefined) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};

UI Components

Product Item Component

The demo uses a sophisticated product item component with cart integration:

components/Screens/Products/components/ProductItem/index.js
import React, {useState} from 'react';
import {
View,
Text,
Image,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import {Icon} from '@rneui/themed';
import ProductDetailModal from '../ProductDetail';
import {actions, useCart} from '../../../../../context/cartContext';
import {CartItem} from '../../../../../models/productItem';
import {
useCreateItemToCart,
useRemoveItemFromCart,
} from '../../../../../graphql/hooks/cartItem';

const ProductItem = ({product}) => {
const {
state: {selectedCurrency, cartId, cartItems},
dispatch,
} = useCart();
const _useCreateItemToCart = useCreateItemToCart();
const _useRemoveItemFromCart = useRemoveItemFromCart();

const [quantity, setQuantity] = useState(1);
const [modalVisible, setModalVisible] = useState(false);
const [loading, setLoading] = useState(false);

const isInCart = cartItems.find(item => item.productId === product.id);

const handleAddToCart = async (_product, currency, _cartId) => {
try {
setLoading(true);

const cartItem = new CartItem({
title: _product.title,
currency: currency,
productId: _product.id,
quantity: quantity,
unitPrice: parseFloat(_product.price),
tax: 0,
image: _product.imageUrl,
productShipping: _product.productShipping,
cartItemId: '',
});

const lineItems = [
{
product_id: cartItem.productId,
quantity: cartItem.quantity,
price_data: {
currency: cartItem.currency,
tax: cartItem.tax,
unit_price: cartItem.unitPrice,
},
},
];

const result = await _useCreateItemToCart.createItemToCart(
_cartId,
lineItems,
);

if (Array.isArray(result?.line_items)) {
const matchingLineItem = result.line_items.find(
item => +item.product_id === cartItem.productId,
);
if (matchingLineItem?.id) {
cartItem.cartItemId = matchingLineItem.id;
dispatch({type: actions.ADD_CART_ITEM, payload: cartItem});
}
} else {
throw new Error('Product could not be added to cart.');
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

const handleRemoveToCart = async (cartItemId, _cartId) => {
try {
setLoading(true);
const result = await _useRemoveItemFromCart.removeItemFromCart(
_cartId,
cartItemId,
);
if (result != null) {
dispatch({type: actions.REMOVE_CART_ITEM, payload: cartItemId});
} else {
throw new Error('Error removing product from cart: result is null');
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

return (
<>
<View style={styles.card}>
<Image source={{uri: product.imageUrl}} style={styles.productImage} />
<View style={styles.infoContainer}>
<Text style={styles.title}>{product.title}</Text>
<Text style={styles.price}>
{`${product.price} ${product.currencyCode}`}
</Text>

<View style={styles.quantityContainer}>
<TouchableOpacity
disabled={!!isInCart}
onPress={() => setQuantity(prev => (prev > 1 ? prev - 1 : 1))}
style={[styles.quantityButton, isInCart && styles.disabledButton]}>
<Icon
name="minus-circle"
type="font-awesome"
size={20}
color={isInCart ? '#ccc' : '#007bff'}
/>
</TouchableOpacity>
<Text style={styles.quantityText}>
{isInCart ? isInCart.quantity : quantity}
</Text>
<TouchableOpacity
disabled={!!isInCart}
onPress={() => setQuantity(prev => prev + 1)}
style={[styles.quantityButton, isInCart && styles.disabledButton]}>
<Icon
name="plus-circle"
type="font-awesome"
size={20}
color={isInCart ? '#ccc' : '#007bff'}
/>
</TouchableOpacity>
</View>

<View style={styles.buttonContainer}>
<TouchableOpacity
style={isInCart ? styles.removeToCartButton : styles.detailsButton}>
<Text
style={styles.buttonText}
onPress={() =>
isInCart
? handleRemoveToCart(isInCart.cartItemId, cartId)
: handleAddToCart(product, selectedCurrency, cartId)
}>
{isInCart ? 'Remove from cart' : 'Add to cart'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.addToCartButton}
onPress={() => setModalVisible(true)}>
<Text style={styles.buttonText}>Detail</Text>
</TouchableOpacity>
</View>
</View>
{loading && (
<View style={styles.overlay}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
)}
</View>

<ProductDetailModal
productId={product.id}
isVisible={modalVisible}
onClose={() => setModalVisible(false)}
/>
</>
);
};

const styles = StyleSheet.create({
card: {
borderRadius: 10,
overflow: 'hidden',
backgroundColor: 'white',
marginBottom: 10,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 5,
shadowOffset: {width: 0, height: 2},
elevation: 3,
flexDirection: 'row',
alignItems: 'center',
},
productImage: {
width: 100,
height: 100,
margin: 10,
borderRadius: 50,
},
infoContainer: {
flex: 1,
padding: 10,
justifyContent: 'center',
},
title: {
fontSize: 18,
fontWeight: 'bold',
},
price: {
fontSize: 16,
color: '#555',
marginVertical: 5,
},
quantityContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginVertical: 10,
},
quantityText: {
fontSize: 15,
fontWeight: 'bold',
minWidth: 40,
textAlign: 'center',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 10,
},
detailsButton: {
backgroundColor: '#007bff',
borderRadius: 5,
padding: 7,
flexGrow: 1,
marginRight: 10,
},
addToCartButton: {
backgroundColor: '#007bff',
borderRadius: 5,
padding: 7,
flexGrow: 2,
},
removeToCartButton: {
backgroundColor: 'red',
borderRadius: 5,
padding: 7,
flexGrow: 2,
marginRight: 10,
},
buttonText: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
},
disabledButton: {
opacity: 0.5,
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255,255,255,0.7)',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
},
});

export default ProductItem;

Payment Screen Component

The demo includes comprehensive payment integration with multiple providers:

components/Screens/Payment/index.js
import React, {useState} from 'react';
import {
View,
Text,
ScrollView,
Image,
StyleSheet,
TouchableOpacity,
} from 'react-native';
import {useCart} from '../../../context/cartContext';
import {RadioButton} from 'react-native-paper';
import {StripePaymentButton} from './components/StripePaymentButton';
import {KlarnaPaymentButton} from './components/KlarnaPaymentButton';

export const PaymentScreen = () => {
const {
state: {cartItems, checkout, selectedCurrency},
} = useCart();

const [selectedProvider, setSelectedProvider] = useState('klarna');

const providers = [
{id: 'klarna', title: 'Klarna', component: KlarnaPaymentButton},
{id: 'stripe', title: 'Stripe', component: StripePaymentButton},
];

const {email, billingAddress, shippingAddress} = checkout;

const totalNumber = cartItems.reduce(
(sum, item) => sum + Number(item.unitPrice) * Number(item.quantity),
0,
);
const total = totalNumber.toFixed(2);

const renderPaymentButton = _providers => {
const p = _providers.find(pr => pr.id === selectedProvider);
if (!p) return null;

const PaymentButtonComponent = p.component;

const stripeProps = {
email,
totalAmount: totalNumber,
currency: selectedCurrency,
onChangeMethod: () => setSelectedProvider('klarna'),
};

return (
<PaymentButtonComponent {...(p.id === 'stripe' ? stripeProps : {})} />
);
};

return (
<ScrollView style={styles.container}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Your Order</Text>
<Text style={styles.infoLabel}>Email:</Text>
<Text style={styles.infoContent}>{email}</Text>
<Text style={styles.infoLabel}>Billing Address:</Text>
<Text style={styles.infoContent}>
{`${billingAddress.first_name} ${billingAddress.last_name}, ${billingAddress.address1}, ${billingAddress.city}`}
</Text>
<Text style={styles.infoLabel}>Shipping Address:</Text>
<Text style={styles.infoContent}>
{`${shippingAddress.first_name} ${shippingAddress.last_name}, ${shippingAddress.address1}, ${shippingAddress.city}`}
</Text>
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Products</Text>
{cartItems.map(item => (
<View style={styles.productItem} key={item.cartItemId.toString()}>
<Image source={{uri: item.image}} style={styles.productImage} />
<View style={styles.productInfo}>
<Text style={styles.productName}>{item.title}</Text>
<Text style={styles.productQuantityPrice}>
{`${item.quantity} x ${item.currency}${item.unitPrice}`}
</Text>
</View>
</View>
))}
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Select Payment Provider</Text>
{providers.map(provider => (
<TouchableOpacity
key={provider.id}
style={styles.providerOption}
onPress={() => setSelectedProvider(provider.id)}>
<Text style={styles.providerText}>{provider.title}</Text>
<RadioButton
value={provider.id}
status={
selectedProvider === provider.id ? 'checked' : 'unchecked'
}
onPress={() => setSelectedProvider(provider.id)}
/>
</TouchableOpacity>
))}
</View>

<View style={styles.totalSection}>
<Text style={styles.totalTitle}>Total:</Text>
<Text style={styles.totalAmount}>
{selectedCurrency}
{total}
</Text>
</View>

{renderPaymentButton(providers)}
</ScrollView>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
paddingTop: 0,
backgroundColor: '#f9f9fb',
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
infoLabel: {
fontWeight: '600',
color: '#666',
marginTop: 5,
},
infoContent: {
color: '#333',
marginTop: 2,
},
productItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
productImage: {
width: 60,
height: 60,
borderRadius: 30,
marginRight: 15,
},
productInfo: {
flex: 1,
},
productName: {
fontSize: 16,
fontWeight: 'bold',
},
productQuantityPrice: {
fontSize: 14,
color: '#666',
},
totalSection: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 5,
paddingTop: 5,
},
totalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
},
totalAmount: {
fontSize: 18,
fontWeight: 'bold',
color: '#E91E63',
},
providerOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#ccc',
},
providerText: {
fontSize: 16,
},
});

export default PaymentScreen;

Main App Component

The main App component integrates all providers and components:

App.tsx
import {ApolloProvider} from '@apollo/client';
import React from 'react';
import {SafeAreaView, StyleSheet, useColorScheme} from 'react-native';

import {Colors} from 'react-native/Libraries/NewAppScreen';
import client from './graphql/client';
import {CartProvider} from './context/cartContext';
import {InitializationMain} from './components/InitializationMain';
import AppHeader from './components/Header';
import FooterNavigation from './components/FooterNavigation';
import {ScreenContainer} from './components/Screens/Container';
import {ReachuSdkProvider} from './context/reachu-sdk-provider';

function App() {
const isDarkMode = useColorScheme() === 'dark';

const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};

return (
<ReachuSdkProvider>
<ApolloProvider client={client}>
<CartProvider>
<InitializationMain>
<SafeAreaView style={[styles.container, backgroundStyle]}>
<AppHeader title="Online Store" />
<ScreenContainer style={styles.screenContainerPadding} />
<FooterNavigation />
</SafeAreaView>
</InitializationMain>
</CartProvider>
</ApolloProvider>
</ReachuSdkProvider>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
screenContainerPadding: {
paddingBottom: 0,
},
});

export default App;

Running the Demo App

1. Clone the Repository

git clone https://github.com/ReachuDevteam/react-native-sdk-demo.git
cd react-native-sdk-demo

2. Install Dependencies

npm install
cd ios && pod install && cd .. # iOS only

3. Configure Environment

Update your environment constants in consts/env.js with your actual API credentials from your Reachu dashboard.

4. Run the App

# iOS
npm run ios

# Android
npm run android

Key Features Demonstrated

Architecture Pattern

  • Context API: State management using React Context
  • Custom Hooks: GraphQL operations wrapped in reusable hooks
  • Component Composition: Modular component structure
  • Provider Pattern: SDK instance management through context

E-commerce Flow

  • Product Browsing: Grid layout with product details
  • Cart Management: Add, remove, update quantities
  • Checkout Process: Customer information collection
  • Payment Integration: Stripe and Klarna support
  • State Persistence: Cart state maintained across screens

Real-world Implementation

  • Error Handling: Comprehensive error management
  • Loading States: User feedback during operations
  • Navigation: Screen-based navigation system
  • Responsive Design: Mobile-optimized UI components

Code Highlights

GraphQL Integration

The demo uses Apollo Client for GraphQL operations with custom hooks:

// Example GraphQL hook usage
const {loading, error, products} = useChannelGetProducts(selectedCurrency);

State Management

Reducer-based state management for cart operations:

const cartReducer = (state, action) => {
switch (action.type) {
case actions.ADD_CART_ITEM:
return {...state, cartItems: [...state.cartItems, action.payload]};
// ... other cases
}
};

Payment Provider Abstraction

Dynamic payment provider selection:

const providers = [
{id: 'klarna', title: 'Klarna', component: KlarnaPaymentButton},
{id: 'stripe', title: 'Stripe', component: StripePaymentButton},
];

Next Steps

To extend this demo for production use:

  1. Add User Authentication - User accounts and order history
  2. Implement Search - Product search and filtering
  3. Add Categories - Product categorization
  4. Order Tracking - Post-purchase order status
  5. Push Notifications - Order updates and promotions
  6. Offline Support - Cart persistence without network
  7. Testing - Unit and integration tests
  8. Performance - Image caching and optimization

This complete example from the official demo repository provides a solid foundation for building production-ready React Native e-commerce applications with the Reachu SDK.