Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ To learn how to use an example, open its `README.md` file. You'll find the detai
| [Product Reviews](./product-reviews/README.md) | Custom Feature | Allow customers to add product reviews, and merchants to manage them. |
| [Quotes Management](./quotes-management/README.md) | Custom Feature | Allow customers to send quotes, and merchants to manage and accept them. |
| [Re-order Feature](./re-order/README.md) | Custom Feature | Allow customers to re-order a previous order. |
| [React Native and Expo Store](./react-native-expo/README.md) | Storefront | Create a mobile app for your Medusa backend with React Native and Expo. |
| [Request Returns from Storefront](./returns-storefront/README.md) | Storefront | Let custmers request a return of their order from the storefront. |
| [Resend Integration](./resend-integration/README.md) | Integration | Integrate Resend to send notifications in Medusa. |
| [Restaurant Marketplace](./restaurant-marketplace/README.md) | Custom Feature | Build an Uber-Eats clone with Medusa. |
Expand Down
2 changes: 2 additions & 0 deletions react-native-expo/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
EXPO_PUBLIC_MEDUSA_PUBLISHABLE_API_KEY=
EXPO_PUBLIC_MEDUSA_URL=
43 changes: 43 additions & 0 deletions react-native-expo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

app-example

# generated native folders
/ios
/android
55 changes: 55 additions & 0 deletions react-native-expo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Medusa v2 Example: React Native / Expo App

This directory holds the code for the [Implement Mobile App with React Native, Expo, and Medusa](https://docs.medusajs.com/resources/storefront-development/guides/react-native-expo) guide.

This codebase only includes the express checkout storefront and doesn't include the Medusa application. You can learn how to install it by following [this guide](https://docs.medusajs.com/learn/installation).

## Installation

1. Clone the repository and change to the `react-native-expo` directory:

```bash
git clone https://github.com/medusajs/examples.git
cd examples/react-native-expo
```

2\. Rename the `.env.template` file to `.env` and set the following variables:

```bash
EXPO_PUBLIC_MEDUSA_PUBLISHABLE_API_KEY=
EXPO_PUBLIC_MEDUSA_URL=
```

Where:

- `EXPO_PUBLIC_MEDUSA_URL` is the URL to your Medusa application server. If the Medusa application is running locally, it should be a local IP. For example `http://192.168.1.100:9000`.
- `EXPO_PUBLIC_MEDUSA_PUBLISHABLE_API_KEY` is the publishable key for your Medusa application. You can retrieve it from the Medusa Admin by going to Settings > Publishable API Keys.

3\. Install dependencies:

```bash
npm install
```

4\. While the Medusa application is running, start the Expo server:

```bash
npm run start
```

You can then test the app on a simulator or with [Expo Go](https://expo.dev/go).

## Testing in a Browser

If you're testing the app on the web, make sure to add `localhost:8081` (default Expo server URL) to the Medusa application's `STORE_CORS` and `AUTH_CORS` environment variables:

```bash
STORE_CORS=previous_values...,http://localhost:8081
AUTH_CORS=previous_values...,http://localhost:8081
```

## More Resources

- [Medusa Documentation](https://docs.medusajs.com)
- [React Native Documentation](https://reactnative.dev/docs/getting-started)
- [Expo Documentation](https://docs.expo.dev/)
48 changes: 48 additions & 0 deletions react-native-expo/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"expo": {
"name": "react-native-store",
"slug": "react-native-store",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "reactnativestore",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#000000"
}
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}
37 changes: 37 additions & 0 deletions react-native-expo/app/(drawer)/(tabs)/(cart)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useColorScheme } from '@/hooks/use-color-scheme';
import { DrawerActions } from '@react-navigation/native';
import { Stack, useNavigation } from 'expo-router';
import React from 'react';
import { TouchableOpacity } from 'react-native';

import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';

export default function CartStackLayout() {
const colorScheme = useColorScheme();
const navigation = useNavigation();
const colors = Colors[colorScheme ?? 'light'];

return (
<Stack
screenOptions={{
headerShown: true,
}}
>
<Stack.Screen
name="index"
options={{
title: 'Cart',
headerLeft: () => (
<TouchableOpacity
onPress={() => navigation.dispatch(DrawerActions.openDrawer())}
style={{ height: 36, width: 36, display: "flex", alignItems: "center", justifyContent: "center" }}
>
<IconSymbol size={28} name="line.3.horizontal" color={colors.icon} />
</TouchableOpacity>
),
}}
/>
</Stack>
);
}
158 changes: 158 additions & 0 deletions react-native-expo/app/(drawer)/(tabs)/(cart)/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { CartItem } from '@/components/cart-item';
import { Loading } from '@/components/loading';
import { Button } from '@/components/ui/button';
import { Colors } from '@/constants/theme';
import { useCart } from '@/context/cart-context';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { formatPrice } from '@/lib/format-price';
import { useRouter } from 'expo-router';
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';

export default function CartScreen() {
const colorScheme = useColorScheme();
const colors = Colors[colorScheme ?? 'light'];
const router = useRouter();
const { cart, updateItemQuantity, removeItem, loading } = useCart();

const isEmpty = !cart?.items || cart.items.length === 0;

if (loading && !cart) {
return <Loading message="Loading cart..." />;
}

if (isEmpty) {
return (
<View style={[styles.emptyContainer, { backgroundColor: colors.background }]}>
<Text style={[styles.emptyTitle, { color: colors.text }]}>Your cart is empty</Text>
<Text style={[styles.emptyText, { color: colors.icon }]}>
Add some products to get started
</Text>
<Button
title="Browse Products"
onPress={() => router.push('/')}
style={styles.browseButton}
/>
</View>
);
}

return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<FlatList
data={cart.items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<CartItem
item={item}
currencyCode={cart.currency_code}
onUpdateQuantity={(quantity) => updateItemQuantity(item.id, quantity)}
onRemove={() => removeItem(item.id)}
/>
)}
contentContainerStyle={styles.listContent}
/>

<View style={[styles.footer, { backgroundColor: colors.background, borderTopColor: colors.icon + '30' }]}>
<View style={styles.totals}>
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { color: colors.text }]}>Subtotal</Text>
<Text style={[styles.totalValue, { color: colors.text }]}>
{formatPrice(cart.item_subtotal, cart.currency_code)}
</Text>
</View>
{cart.tax_total !== undefined && cart.tax_total > 0 && (
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { color: colors.text }]}>Tax</Text>
<Text style={[styles.totalValue, { color: colors.text }]}>
{formatPrice(cart.tax_total, cart.currency_code)}
</Text>
</View>
)}
{cart.shipping_total !== undefined && cart.shipping_total > 0 && (
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { color: colors.text }]}>Shipping</Text>
<Text style={[styles.totalValue, { color: colors.text }]}>
{formatPrice(cart.shipping_total, cart.currency_code)}
</Text>
</View>
)}
<View style={[styles.totalRow, styles.grandTotalRow, { borderTopColor: colors.border }]}>
<Text style={[styles.grandTotalLabel, { color: colors.text }]}>Total</Text>
<Text style={[styles.grandTotalValue, { color: colors.tint }]}>
{formatPrice(cart.total, cart.currency_code)}
</Text>
</View>
</View>
<Button
title="Proceed to Checkout"
onPress={() => {
// TODO navigate to checkout screen
}}
loading={loading}
/>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 40,
},
emptyTitle: {
fontSize: 24,
fontWeight: '700',
marginBottom: 12,
},
emptyText: {
fontSize: 16,
textAlign: 'center',
marginBottom: 32,
},
browseButton: {
minWidth: 200,
},
listContent: {
paddingBottom: 20,
},
footer: {
padding: 16,
borderTopWidth: 1,
},
totals: {
marginBottom: 20,
},
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
totalLabel: {
fontSize: 14,
},
totalValue: {
fontSize: 14,
fontWeight: '500',
},
grandTotalRow: {
marginTop: 8,
paddingTop: 12,
borderTopWidth: 1,
},
grandTotalLabel: {
fontSize: 18,
fontWeight: '700',
},
grandTotalValue: {
fontSize: 20,
fontWeight: '700',
},
});
Loading