Skip to content

Commit dc7d700

Browse files
authored
feat: react-native and expo app (#87)
* feat: react-native and expo app * add missing router
1 parent 2b2cb56 commit dc7d700

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+4737
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ To learn how to use an example, open its `README.md` file. You'll find the detai
3939
| [Product Reviews](./product-reviews/README.md) | Custom Feature | Allow customers to add product reviews, and merchants to manage them. |
4040
| [Quotes Management](./quotes-management/README.md) | Custom Feature | Allow customers to send quotes, and merchants to manage and accept them. |
4141
| [Re-order Feature](./re-order/README.md) | Custom Feature | Allow customers to re-order a previous order. |
42+
| [React Native and Expo Store](./react-native-expo/README.md) | Storefront | Create a mobile app for your Medusa backend with React Native and Expo. |
4243
| [Request Returns from Storefront](./returns-storefront/README.md) | Storefront | Let custmers request a return of their order from the storefront. |
4344
| [Resend Integration](./resend-integration/README.md) | Integration | Integrate Resend to send notifications in Medusa. |
4445
| [Restaurant Marketplace](./restaurant-marketplace/README.md) | Custom Feature | Build an Uber-Eats clone with Medusa. |

react-native-expo/.env.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
EXPO_PUBLIC_MEDUSA_PUBLISHABLE_API_KEY=
2+
EXPO_PUBLIC_MEDUSA_URL=

react-native-expo/.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2+
3+
# dependencies
4+
node_modules/
5+
6+
# Expo
7+
.expo/
8+
dist/
9+
web-build/
10+
expo-env.d.ts
11+
12+
# Native
13+
.kotlin/
14+
*.orig.*
15+
*.jks
16+
*.p8
17+
*.p12
18+
*.key
19+
*.mobileprovision
20+
21+
# Metro
22+
.metro-health-check*
23+
24+
# debug
25+
npm-debug.*
26+
yarn-debug.*
27+
yarn-error.*
28+
29+
# macOS
30+
.DS_Store
31+
*.pem
32+
33+
# local env files
34+
.env*.local
35+
36+
# typescript
37+
*.tsbuildinfo
38+
39+
app-example
40+
41+
# generated native folders
42+
/ios
43+
/android

react-native-expo/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Medusa v2 Example: React Native / Expo App
2+
3+
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.
4+
5+
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).
6+
7+
## Installation
8+
9+
1. Clone the repository and change to the `react-native-expo` directory:
10+
11+
```bash
12+
git clone https://github.com/medusajs/examples.git
13+
cd examples/react-native-expo
14+
```
15+
16+
2\. Rename the `.env.template` file to `.env` and set the following variables:
17+
18+
```bash
19+
EXPO_PUBLIC_MEDUSA_PUBLISHABLE_API_KEY=
20+
EXPO_PUBLIC_MEDUSA_URL=
21+
```
22+
23+
Where:
24+
25+
- `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`.
26+
- `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.
27+
28+
3\. Install dependencies:
29+
30+
```bash
31+
npm install
32+
```
33+
34+
4\. While the Medusa application is running, start the Expo server:
35+
36+
```bash
37+
npm run start
38+
```
39+
40+
You can then test the app on a simulator or with [Expo Go](https://expo.dev/go).
41+
42+
## Testing in a Browser
43+
44+
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:
45+
46+
```bash
47+
STORE_CORS=previous_values...,http://localhost:8081
48+
AUTH_CORS=previous_values...,http://localhost:8081
49+
```
50+
51+
## More Resources
52+
53+
- [Medusa Documentation](https://docs.medusajs.com)
54+
- [React Native Documentation](https://reactnative.dev/docs/getting-started)
55+
- [Expo Documentation](https://docs.expo.dev/)

react-native-expo/app.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"expo": {
3+
"name": "react-native-store",
4+
"slug": "react-native-store",
5+
"version": "1.0.0",
6+
"orientation": "portrait",
7+
"icon": "./assets/images/icon.png",
8+
"scheme": "reactnativestore",
9+
"userInterfaceStyle": "automatic",
10+
"newArchEnabled": true,
11+
"ios": {
12+
"supportsTablet": true
13+
},
14+
"android": {
15+
"adaptiveIcon": {
16+
"backgroundColor": "#E6F4FE",
17+
"foregroundImage": "./assets/images/android-icon-foreground.png",
18+
"backgroundImage": "./assets/images/android-icon-background.png",
19+
"monochromeImage": "./assets/images/android-icon-monochrome.png"
20+
},
21+
"edgeToEdgeEnabled": true,
22+
"predictiveBackGestureEnabled": false
23+
},
24+
"web": {
25+
"output": "static",
26+
"favicon": "./assets/images/favicon.png"
27+
},
28+
"plugins": [
29+
"expo-router",
30+
[
31+
"expo-splash-screen",
32+
{
33+
"image": "./assets/images/splash-icon.png",
34+
"imageWidth": 200,
35+
"resizeMode": "contain",
36+
"backgroundColor": "#ffffff",
37+
"dark": {
38+
"backgroundColor": "#000000"
39+
}
40+
}
41+
]
42+
],
43+
"experiments": {
44+
"typedRoutes": true,
45+
"reactCompiler": true
46+
}
47+
}
48+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useColorScheme } from '@/hooks/use-color-scheme';
2+
import { DrawerActions } from '@react-navigation/native';
3+
import { Stack, useNavigation } from 'expo-router';
4+
import React from 'react';
5+
import { TouchableOpacity } from 'react-native';
6+
7+
import { IconSymbol } from '@/components/ui/icon-symbol';
8+
import { Colors } from '@/constants/theme';
9+
10+
export default function CartStackLayout() {
11+
const colorScheme = useColorScheme();
12+
const navigation = useNavigation();
13+
const colors = Colors[colorScheme ?? 'light'];
14+
15+
return (
16+
<Stack
17+
screenOptions={{
18+
headerShown: true,
19+
}}
20+
>
21+
<Stack.Screen
22+
name="index"
23+
options={{
24+
title: 'Cart',
25+
headerLeft: () => (
26+
<TouchableOpacity
27+
onPress={() => navigation.dispatch(DrawerActions.openDrawer())}
28+
style={{ height: 36, width: 36, display: "flex", alignItems: "center", justifyContent: "center" }}
29+
>
30+
<IconSymbol size={28} name="line.3.horizontal" color={colors.icon} />
31+
</TouchableOpacity>
32+
),
33+
}}
34+
/>
35+
</Stack>
36+
);
37+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { CartItem } from '@/components/cart-item';
2+
import { Loading } from '@/components/loading';
3+
import { Button } from '@/components/ui/button';
4+
import { Colors } from '@/constants/theme';
5+
import { useCart } from '@/context/cart-context';
6+
import { useColorScheme } from '@/hooks/use-color-scheme';
7+
import { formatPrice } from '@/lib/format-price';
8+
import { useRouter } from 'expo-router';
9+
import React from 'react';
10+
import { FlatList, StyleSheet, Text, View } from 'react-native';
11+
12+
export default function CartScreen() {
13+
const colorScheme = useColorScheme();
14+
const colors = Colors[colorScheme ?? 'light'];
15+
const router = useRouter();
16+
const { cart, updateItemQuantity, removeItem, loading } = useCart();
17+
18+
const isEmpty = !cart?.items || cart.items.length === 0;
19+
20+
if (loading && !cart) {
21+
return <Loading message="Loading cart..." />;
22+
}
23+
24+
if (isEmpty) {
25+
return (
26+
<View style={[styles.emptyContainer, { backgroundColor: colors.background }]}>
27+
<Text style={[styles.emptyTitle, { color: colors.text }]}>Your cart is empty</Text>
28+
<Text style={[styles.emptyText, { color: colors.icon }]}>
29+
Add some products to get started
30+
</Text>
31+
<Button
32+
title="Browse Products"
33+
onPress={() => router.push('/')}
34+
style={styles.browseButton}
35+
/>
36+
</View>
37+
);
38+
}
39+
40+
return (
41+
<View style={[styles.container, { backgroundColor: colors.background }]}>
42+
<FlatList
43+
data={cart.items}
44+
keyExtractor={(item) => item.id}
45+
renderItem={({ item }) => (
46+
<CartItem
47+
item={item}
48+
currencyCode={cart.currency_code}
49+
onUpdateQuantity={(quantity) => updateItemQuantity(item.id, quantity)}
50+
onRemove={() => removeItem(item.id)}
51+
/>
52+
)}
53+
contentContainerStyle={styles.listContent}
54+
/>
55+
56+
<View style={[styles.footer, { backgroundColor: colors.background, borderTopColor: colors.icon + '30' }]}>
57+
<View style={styles.totals}>
58+
<View style={styles.totalRow}>
59+
<Text style={[styles.totalLabel, { color: colors.text }]}>Subtotal</Text>
60+
<Text style={[styles.totalValue, { color: colors.text }]}>
61+
{formatPrice(cart.item_subtotal, cart.currency_code)}
62+
</Text>
63+
</View>
64+
{cart.tax_total !== undefined && cart.tax_total > 0 && (
65+
<View style={styles.totalRow}>
66+
<Text style={[styles.totalLabel, { color: colors.text }]}>Tax</Text>
67+
<Text style={[styles.totalValue, { color: colors.text }]}>
68+
{formatPrice(cart.tax_total, cart.currency_code)}
69+
</Text>
70+
</View>
71+
)}
72+
{cart.shipping_total !== undefined && cart.shipping_total > 0 && (
73+
<View style={styles.totalRow}>
74+
<Text style={[styles.totalLabel, { color: colors.text }]}>Shipping</Text>
75+
<Text style={[styles.totalValue, { color: colors.text }]}>
76+
{formatPrice(cart.shipping_total, cart.currency_code)}
77+
</Text>
78+
</View>
79+
)}
80+
<View style={[styles.totalRow, styles.grandTotalRow, { borderTopColor: colors.border }]}>
81+
<Text style={[styles.grandTotalLabel, { color: colors.text }]}>Total</Text>
82+
<Text style={[styles.grandTotalValue, { color: colors.tint }]}>
83+
{formatPrice(cart.total, cart.currency_code)}
84+
</Text>
85+
</View>
86+
</View>
87+
<Button
88+
title="Proceed to Checkout"
89+
onPress={() => router.push("/checkout")}
90+
loading={loading}
91+
/>
92+
</View>
93+
</View>
94+
);
95+
}
96+
97+
const styles = StyleSheet.create({
98+
container: {
99+
flex: 1,
100+
},
101+
emptyContainer: {
102+
flex: 1,
103+
justifyContent: 'center',
104+
alignItems: 'center',
105+
padding: 40,
106+
},
107+
emptyTitle: {
108+
fontSize: 24,
109+
fontWeight: '700',
110+
marginBottom: 12,
111+
},
112+
emptyText: {
113+
fontSize: 16,
114+
textAlign: 'center',
115+
marginBottom: 32,
116+
},
117+
browseButton: {
118+
minWidth: 200,
119+
},
120+
listContent: {
121+
paddingBottom: 20,
122+
},
123+
footer: {
124+
padding: 16,
125+
borderTopWidth: 1,
126+
},
127+
totals: {
128+
marginBottom: 20,
129+
},
130+
totalRow: {
131+
flexDirection: 'row',
132+
justifyContent: 'space-between',
133+
alignItems: 'center',
134+
marginBottom: 12,
135+
},
136+
totalLabel: {
137+
fontSize: 14,
138+
},
139+
totalValue: {
140+
fontSize: 14,
141+
fontWeight: '500',
142+
},
143+
grandTotalRow: {
144+
marginTop: 8,
145+
paddingTop: 12,
146+
borderTopWidth: 1,
147+
},
148+
grandTotalLabel: {
149+
fontSize: 18,
150+
fontWeight: '700',
151+
},
152+
grandTotalValue: {
153+
fontSize: 20,
154+
fontWeight: '700',
155+
},
156+
});

0 commit comments

Comments
 (0)