├── .expo-shared
└── assets.json
├── .gitignore
├── App.js
├── README.md
├── app.json
├── assets
├── adaptive-icon.png
├── favicon.png
├── icon.png
└── splash.png
├── babel.config.js
├── components
├── Button.js
├── CartItem.js
├── Header.js
├── Payment.js
├── ProductCard.js
├── ProductInfo
│ ├── Image.js
│ └── MetaInfo.js
├── RadioButton.js
├── ShippingAddress.js
└── Toast.js
├── constants
├── stripe.js
└── url.js
├── cover.png
├── package.json
├── screens
├── Cart.js
├── Checkout.js
├── ProductInfo.js
└── Products.js
└── yarn.lock
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import { Router, Scene, Stack } from "react-native-router-flux";
2 | import Products from "./screens/Products";
3 | import ProductInfo from "./screens/ProductInfo";
4 | import axios from "axios";
5 | import baseURL from "./constants/url";
6 | import { useEffect } from "react";
7 | import Cart from "./screens/Cart";
8 | import Checkout from "./screens/Checkout";
9 | import { Provider as PaperProvider } from "react-native-paper";
10 | import AsyncStorage from "@react-native-async-storage/async-storage";
11 | import { publishable_key } from "./constants/stripe";
12 | import { StripeProvider } from "@stripe/stripe-react-native";
13 | export default function App() {
14 | const getCartId = () => {
15 | axios.post(`${baseURL}/store/carts`).then((res) => {
16 | AsyncStorage.setItem("cart_id", res.data.cart.id);
17 | });
18 | };
19 | // Check cart_id
20 | const checkCartId = async () => {
21 | const cartId = await AsyncStorage.getItem("cart_id");
22 | if (!cartId) {
23 | getCartId();
24 | }
25 | };
26 |
27 | useEffect(() => {
28 | checkCartId();
29 | }, []);
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## React Native Medusa
4 | 
5 |
6 | ## About
7 |
8 | ### Participants
9 | Suhail - @SuhailKakar
10 |
11 | ### Description
12 |
13 | An open source ecommerce mobile application built using Medusa and React Native Expo. It includes products screen, cart, checkout and payment.
14 |
15 | ### Preview
16 |
17 | 
18 |
19 |
20 | ## Set up Project
21 |
22 | ### Prerequisites
23 | Before you start with the tutorial make sure you have
24 |
25 | - [Node.js](https://nodejs.org/en/) v14 or greater installed on your machine
26 | - [Expo CLI](https://expo.dev/)
27 | - [Medusa server](https://docs.medusajs.com/quickstart/quick-start/) v14 or greater installed on your machine
28 | - Stripe account
29 | - [Stripe plugin](https://docs.medusajs.com/add-plugins/stripe/) is required on the Medusa server
30 |
31 | ### Install Project
32 |
33 | 1. Clone the repository:
34 |
35 | ```bash
36 | git clone https://github.com/suhailkakar/react-native-medusajs
37 | ```
38 |
39 | 2. Change directory and install dependencies:
40 |
41 | ```bash
42 | cd react-native-medusajs
43 | npm install
44 | ```
45 | 4. Start the app
46 | ```
47 | expo start
48 | ```
49 |
50 | ## Resources
51 | - [Medusa’s GitHub repository](https://github.com/medusajs/medusa)
52 | - [Medusa Admin Panel](https://github.com/medusajs/admin)
53 | - [Medusa Documentation](https://docs.medusajs.com/)
54 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "medusa-store",
4 | "slug": "medusa-store",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "updates": {
15 | "fallbackToCacheTimeout": 0
16 | },
17 | "assetBundlePatterns": [
18 | "**/*"
19 | ],
20 | "ios": {
21 | "supportsTablet": true
22 | },
23 | "android": {
24 | "adaptiveIcon": {
25 | "foregroundImage": "./assets/adaptive-icon.png",
26 | "backgroundColor": "#FFFFFF"
27 | }
28 | },
29 | "plugins": [
30 | [
31 | "@stripe/stripe-react-native",
32 | {
33 | "enableGooglePay": false
34 | }
35 | ]
36 | ],
37 | "web": {
38 | "favicon": "./assets/favicon.png"
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/react-native-medusajs/2fe82f3c566e9ac44164aa99954b25652a73fb03/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/react-native-medusajs/2fe82f3c566e9ac44164aa99954b25652a73fb03/assets/favicon.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/react-native-medusajs/2fe82f3c566e9ac44164aa99954b25652a73fb03/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/react-native-medusajs/2fe82f3c566e9ac44164aa99954b25652a73fb03/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | plugins: ["react-native-reanimated/plugin"],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/components/Button.js:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React from "react";
3 | import { widthToDp } from "rn-responsive-screen";
4 |
5 | export default function Button({ title, onPress, style, textSize, large }) {
6 | return (
7 |
8 |
16 | {title}
17 |
18 |
19 | );
20 | }
21 |
22 | const styles = StyleSheet.create({
23 | container: {
24 | backgroundColor: "#C37AFF",
25 | padding: 5,
26 | width: widthToDp(20),
27 | alignItems: "center",
28 | justifyContent: "center",
29 | borderRadius: 59,
30 | },
31 | large: {
32 | width: "100%",
33 | marginTop: 10,
34 | height: widthToDp(12),
35 | },
36 | text: {
37 | color: "#fff",
38 | fontWeight: "bold",
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/components/CartItem.js:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet, Image } from "react-native";
2 | import React from "react";
3 | import { heightToDp, width, widthToDp } from "rn-responsive-screen";
4 |
5 | export default function CartItem({ product }) {
6 | return (
7 |
8 |
9 |
10 |
11 | {product.title}
12 |
13 | {product.description} • ${product.unit_price / 100}
14 |
15 |
16 |
17 | ${product.total / 100}
18 | x{product.quantity}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | const styles = StyleSheet.create({
26 | container: {
27 | marginTop: 20,
28 | flexDirection: "row",
29 | borderBottomWidth: 1,
30 | paddingBottom: 10,
31 | borderColor: "#e6e6e6",
32 | width: widthToDp("90%"),
33 | },
34 | image: {
35 | width: widthToDp(30),
36 | height: heightToDp(30),
37 | borderRadius: 10,
38 | },
39 | title: {
40 | fontSize: widthToDp(4),
41 | fontWeight: "bold",
42 | },
43 | footer: {
44 | flexDirection: "row",
45 | justifyContent: "space-between",
46 | },
47 | info: {
48 | marginLeft: widthToDp(3),
49 | flexDirection: "column",
50 | justifyContent: "space-between",
51 | marginVertical: heightToDp(2),
52 | width: widthToDp(50),
53 | },
54 | description: {
55 | fontSize: widthToDp(3.5),
56 | color: "#8e8e93",
57 | marginTop: heightToDp(2),
58 | },
59 |
60 | price: {
61 | fontSize: widthToDp(4),
62 | },
63 | quantity: {
64 | fontSize: widthToDp(4),
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import { View, Image, StyleSheet, Text } from "react-native";
2 | import React from "react";
3 | import { widthToDp } from "rn-responsive-screen";
4 |
5 | export default function Header({ title }) {
6 | return (
7 |
8 |
14 | {title}
15 |
16 | );
17 | }
18 | const styles = StyleSheet.create({
19 | container: {
20 | flexDirection: "row",
21 | alignItems: "center",
22 | justifyContent: "center",
23 | width: widthToDp(100),
24 | marginBottom: 10,
25 | },
26 | title: {
27 | fontSize: 20,
28 | fontWeight: "500",
29 | },
30 | logo: {
31 | width: 50,
32 | height: 50,
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/components/Payment.js:
--------------------------------------------------------------------------------
1 | import { View, Text } from "react-native";
2 | import React from "react";
3 | import {
4 | CreditCardInput,
5 | LiteCreditCardInput,
6 | } from "react-native-credit-card-input";
7 | export default function Payment({ onChange }) {
8 | return (
9 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/ProductCard.js:
--------------------------------------------------------------------------------
1 | import { View, Text, Image, StyleSheet } from "react-native";
2 | import React from "react";
3 | import { widthToDp, heightToDp } from "rn-responsive-screen";
4 | import Button from "./Button";
5 |
6 | export default function ProductCard({ key, product }) {
7 | return (
8 |
9 |
15 | {product.title}
16 | {product.handle}
17 |
18 |
19 | ${product.variants[0].prices[1].amount / 100}
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | const styles = StyleSheet.create({
29 | container: {
30 | shadowColor: "#000",
31 | borderRadius: 10,
32 | marginBottom: heightToDp(4),
33 | shadowOffset: {
34 | width: 2,
35 | height: 5,
36 | },
37 | shadowOpacity: 0.25,
38 | shadowRadius: 6.84,
39 | elevation: 5,
40 | padding: 10,
41 | width: widthToDp(42),
42 | backgroundColor: "#fff",
43 | },
44 | image: {
45 | height: heightToDp(40),
46 | borderRadius: 7,
47 | marginBottom: heightToDp(2),
48 | },
49 | title: {
50 | fontSize: widthToDp(3.7),
51 | fontWeight: "bold",
52 | },
53 | priceContainer: {
54 | flexDirection: "row",
55 | justifyContent: "space-between",
56 | alignItems: "center",
57 | marginTop: heightToDp(3),
58 | },
59 | category: {
60 | fontSize: widthToDp(3.4),
61 | color: "#828282",
62 | marginTop: 3,
63 | },
64 | price: {
65 | fontSize: widthToDp(4),
66 | fontWeight: "bold",
67 | },
68 | });
69 |
--------------------------------------------------------------------------------
/components/ProductInfo/Image.js:
--------------------------------------------------------------------------------
1 | import { View, TouchableOpacity, Image, StyleSheet } from "react-native";
2 | import React, { useEffect, useState } from "react";
3 | import { widthToDp } from "rn-responsive-screen";
4 |
5 | export default function Images({ images }) {
6 | const [activeImage, setActiveImage] = useState(null);
7 |
8 | useEffect(() => {
9 | setActiveImage(images[0].url);
10 | }, []);
11 |
12 | return (
13 |
14 |
15 |
16 | {images.map((image, index) => (
17 | {
20 | setActiveImage(image.url);
21 | }}
22 | >
23 |
32 |
33 | ))}
34 |
35 |
36 | );
37 | }
38 |
39 | const styles = StyleSheet.create({
40 | image: {
41 | width: widthToDp(100),
42 | height: widthToDp(100),
43 | },
44 | previewContainer: {
45 | flexDirection: "row",
46 | justifyContent: "center",
47 | alignItems: "center",
48 | marginTop: widthToDp(-10),
49 | },
50 | imageContainer: {
51 | backgroundColor: "#F7F6FB",
52 | paddingBottom: widthToDp(10),
53 | },
54 | imagePreview: {
55 | width: widthToDp(15),
56 | marginRight: widthToDp(5),
57 | borderColor: "#C37AFF",
58 | borderRadius: 10,
59 | height: widthToDp(15),
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/components/ProductInfo/MetaInfo.js:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React, { useState } from "react";
3 | import { height, heightToDp } from "rn-responsive-screen";
4 | import { TouchableOpacity } from "react-native-gesture-handler";
5 | import Button from "../Button";
6 | import axios from "axios";
7 | import baseURL from "../../constants/url";
8 | import AsyncStorage from "@react-native-async-storage/async-storage";
9 | export default function MetaInfo({ product }) {
10 | const [activeSize, setActiveSize] = useState(0);
11 |
12 | const addToCart = async () => {
13 | const cartId = await AsyncStorage.getItem("cart_id");
14 |
15 | axios
16 | .post(baseURL + "/store/carts/" + cartId + "/line-items", {
17 | variant_id: product.variants[0].id,
18 | quantity: 1,
19 | })
20 | .then(({ data }) => {
21 | alert(`Item ${product.title} added to cart`);
22 | })
23 | .catch((err) => {
24 | console.log(err);
25 | });
26 | };
27 |
28 | return (
29 |
30 |
31 | {product.title}
32 |
33 |
34 | ${product.variants[0].prices[1].amount / 100}
35 |
36 | ⭐⭐⭐
37 |
38 |
39 | Available Sizes
40 |
41 | {product.options[0].values.map((size, index) => (
42 | setActiveSize(index)}>
43 |
51 | {size.value}
52 |
53 |
54 | ))}
55 |
56 | Description
57 | {product.description}
58 |
59 |
60 | );
61 | }
62 |
63 | const styles = StyleSheet.create({
64 | container: {
65 | marginTop: heightToDp(-5),
66 | backgroundColor: "#fff",
67 | borderTopLeftRadius: 20,
68 | borderTopRightRadius: 20,
69 | padding: heightToDp(5),
70 | },
71 | title: {
72 | fontSize: heightToDp(6),
73 | fontWeight: "bold",
74 | },
75 | row: {
76 | flexDirection: "row",
77 | justifyContent: "space-between",
78 | alignItems: "center",
79 | },
80 | price: {
81 | fontSize: heightToDp(5),
82 | fontWeight: "bold",
83 | color: "#C37AFF",
84 | },
85 | heading: {
86 | fontSize: heightToDp(5),
87 | marginTop: heightToDp(3),
88 | },
89 | star: {
90 | fontSize: heightToDp(3),
91 | marginTop: heightToDp(1),
92 | },
93 | sizeTag: {
94 | borderColor: "#C37AFF",
95 | backgroundColor: "#F7F6FB",
96 | color: "#000",
97 | paddingHorizontal: heightToDp(7),
98 | paddingVertical: heightToDp(2),
99 | borderRadius: heightToDp(2),
100 | marginTop: heightToDp(2),
101 | overflow: "hidden",
102 | fontSize: heightToDp(4),
103 | marginBottom: heightToDp(2),
104 | },
105 | description: {
106 | fontSize: heightToDp(4),
107 | color: "#aaa",
108 | marginTop: heightToDp(2),
109 | },
110 | });
111 |
--------------------------------------------------------------------------------
/components/RadioButton.js:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
2 | import React from "react";
3 |
4 | const RadioButton = ({ onPress, selected, children }) => {
5 | return (
6 |
7 |
8 | {selected ? : null}
9 |
10 |
11 | {children}
12 |
13 |
14 | );
15 | };
16 | const styles = StyleSheet.create({
17 | radioButtonContainer: {
18 | flexDirection: "row",
19 | alignItems: "center",
20 | marginRight: 45,
21 | },
22 | radioButton: {
23 | height: 20,
24 | width: 20,
25 | backgroundColor: "#F8F8F8",
26 | borderRadius: 10,
27 | borderWidth: 1,
28 | borderColor: "#E6E6E6",
29 | alignItems: "center",
30 | justifyContent: "center",
31 | },
32 | radioButtonIcon: {
33 | height: 14,
34 | width: 14,
35 | borderRadius: 7,
36 | backgroundColor: "#C37AFF",
37 | },
38 | radioButtonText: {
39 | fontSize: 16,
40 | marginLeft: 16,
41 | },
42 | });
43 | export default RadioButton;
44 |
--------------------------------------------------------------------------------
/components/ShippingAddress.js:
--------------------------------------------------------------------------------
1 | // Importing a few package and components
2 | import { View, StyleSheet, Text, TextInput } from "react-native";
3 | import React, { useState } from "react";
4 | import { heightToDp } from "rn-responsive-screen";
5 |
6 | export default function ShippingAddress({ onChange }) {
7 | // Passing onChange as a prop
8 |
9 | // Declaring a few states to store the user's input
10 | const [firstName, setFirstName] = useState("");
11 | const [lastName, setLastName] = useState("");
12 | const [AddressLine1, setAddressLine1] = useState("");
13 | const [AddressLine2, setAddressLine2] = useState("");
14 | const [city, setCity] = useState("");
15 | const [country, setCountry] = useState("");
16 | const [province, setProvince] = useState("");
17 | const [postalCode, setPostalCode] = useState("");
18 | const [phone, setPhone] = useState("");
19 | const [company, setCompany] = useState("");
20 |
21 | // Function to handle the user's input
22 | const handleChange = () => {
23 | // Creating an object to store the user's input
24 | let address = {
25 | first_name: firstName,
26 | last_name: lastName,
27 | address_1: AddressLine1,
28 | address_2: AddressLine2,
29 | city,
30 | province,
31 | postal_code: postalCode,
32 | phone,
33 | company,
34 | };
35 | // Calling the onChange function and passing the address object as an argument
36 | onChange(address);
37 | };
38 |
39 | return (
40 | // Creating a view to hold the user's input
41 |
42 | {/* Creating a text input for the user's first name */}
43 | {
45 | // Setting the user's input to the firstName state
46 | setFirstName(e);
47 | // Calling the handleChange function
48 | handleChange();
49 | }}
50 | placeholder="First Name"
51 | style={styles.input}
52 | />
53 | {
55 | setLastName(e);
56 | handleChange();
57 | }}
58 | placeholder="Last Name"
59 | style={styles.input}
60 | />
61 |
62 | {
64 | setAddressLine1(e);
65 | handleChange();
66 | }}
67 | placeholder="Address Line 1"
68 | style={styles.input}
69 | />
70 | {
72 | setAddressLine2(e);
73 | handleChange();
74 | }}
75 | placeholder="Address Line 2"
76 | style={styles.input}
77 | />
78 | {
80 | setCity(e);
81 | handleChange();
82 | }}
83 | placeholder="City"
84 | style={styles.input}
85 | />
86 | {
88 | setCountry(e);
89 | handleChange();
90 | }}
91 | placeholder="Country"
92 | style={styles.input}
93 | />
94 | {
96 | setProvince(e);
97 | handleChange();
98 | }}
99 | placeholder="Province"
100 | style={styles.input}
101 | />
102 | {
104 | setPostalCode(e);
105 | handleChange();
106 | }}
107 | placeholder="Postal Code"
108 | style={styles.input}
109 | />
110 | {
112 | setPhone(e);
113 | handleChange();
114 | }}
115 | placeholder="Phone"
116 | style={styles.input}
117 | />
118 | {
120 | setCompany(e);
121 | handleChange();
122 | }}
123 | placeholder="Company"
124 | style={styles.input}
125 | />
126 |
127 | );
128 | }
129 |
130 | // Creating a stylesheet to style the view
131 | const styles = StyleSheet.create({
132 | container: {
133 | marginTop: heightToDp(2),
134 | },
135 | input: {
136 | borderWidth: 1,
137 | padding: 12,
138 | borderColor: "#E5E5E5",
139 | borderRadius: 5,
140 | marginTop: 10.2,
141 | },
142 | });
143 |
--------------------------------------------------------------------------------
/components/Toast.js:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React from "react";
3 | import { widthToDp } from "rn-responsive-screen";
4 | import { Feather } from "@expo/vector-icons";
5 |
6 | export default function Toast({ message }) {
7 | return (
8 |
9 |
10 | {message}
11 |
12 | );
13 | }
14 |
15 | const styles = StyleSheet.create({
16 | container: {
17 | backgroundColor: "#f7e6ff",
18 | borderRadius: 100,
19 | width: widthToDp("80%"),
20 | alignItems: "center",
21 | justifyContent: "center",
22 | paddingVertical: 12,
23 | flexDirection: "row",
24 | },
25 | text: {
26 | color: "#D583FF",
27 | marginLeft: 10,
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/constants/stripe.js:
--------------------------------------------------------------------------------
1 | const secret_key = "YOUR_STRIPE_SECRET_KEY";
2 | const publishable_key = "YOUR_STRIPE_PUBLISHABLE_KEY";
3 |
4 | export { secret_key, publishable_key };
5 |
--------------------------------------------------------------------------------
/constants/url.js:
--------------------------------------------------------------------------------
1 | const baseURL = "http://127.0.0.1:9000";
2 |
3 | export default baseURL;
4 |
--------------------------------------------------------------------------------
/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/react-native-medusajs/2fe82f3c566e9ac44164aa99954b25652a73fb03/cover.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medusa-store",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web",
10 | "eject": "expo eject"
11 | },
12 | "dependencies": {
13 | "@expo/vector-icons": "^13.0.0",
14 | "@react-native-async-storage/async-storage": "~1.17.3",
15 | "@stripe/stripe-react-native": "0.6.1",
16 | "axios": "^0.27.2",
17 | "expo": "~45.0.0",
18 | "expo-status-bar": "~1.3.0",
19 | "react": "17.0.2",
20 | "react-dom": "17.0.2",
21 | "react-native": "0.68.2",
22 | "react-native-credit-card-input": "^0.4.1",
23 | "react-native-gesture-handler": "~2.2.1",
24 | "react-native-paper": "^4.12.4",
25 | "react-native-reanimated": "~2.8.0",
26 | "react-native-router-flux": "^4.3.1",
27 | "react-native-safe-area-context": "4.2.4",
28 | "react-native-screens": "~3.11.1",
29 | "react-native-web": "0.17.7",
30 | "rn-responsive-screen": "^1.1.9"
31 | },
32 | "devDependencies": {
33 | "@babel/core": "^7.12.9"
34 | },
35 | "private": true
36 | }
37 |
--------------------------------------------------------------------------------
/screens/Cart.js:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React, { useEffect, useState } from "react";
3 | import Header from "../components/Header";
4 | import axios from "axios";
5 | import baseURL from "../constants/url";
6 | import Toast from "../components/Toast";
7 | import CartItem from "../components/CartItem";
8 | import { ScrollView } from "react-native-gesture-handler";
9 | import { SafeAreaView } from "react-native-safe-area-context";
10 | import { width, widthToDp } from "rn-responsive-screen";
11 | import Button from "../components/Button";
12 | import { Actions } from "react-native-router-flux";
13 | import AsyncStorage from "@react-native-async-storage/async-storage";
14 |
15 | export default function Cart() {
16 | const [cart, setCart] = useState([]);
17 |
18 | const fetchCart = async () => {
19 | // Get the cart id from the device storage
20 | const cartId = await AsyncStorage.getItem("cart_id");
21 | // Fetch the products from the cart API using the cart id
22 | axios.get(`${baseURL}/store/carts/${cartId}`).then(({ data }) => {
23 | // Set the cart state to the products in the cart
24 | setCart(data.cart);
25 | });
26 | };
27 |
28 | useEffect(() => {
29 | // Calling the fetchCart function when the component mounts
30 | fetchCart();
31 | }, []);
32 | return (
33 | // SafeAreaView is used to avoid the notch on the phone
34 |
35 |
36 | {/* SchrollView is used in order to scroll the content */}
37 |
38 | {/* Using the reusable header component */}
39 |
40 |
41 | {/* Mapping the products into the Cart component */}
42 | {cart?.items?.map((product) => (
43 |
44 | ))}
45 |
46 | {/* Creating a seperate view to show the total amount and checkout button */}
47 |
48 |
49 | Items
50 |
51 | {/* Showing Cart Total */}
52 |
60 | {/* Dividing the total by 100 because Medusa doesn't store numbers in decimal */}
61 | ${cart?.total / 100}
62 |
63 |
64 |
65 | {/* Showing the discount (if any) */}
66 | Discount
67 |
75 | - ${cart?.discount_total / 100}
76 |
77 |
78 |
79 | Total
80 |
88 | {/* Calculating the total */}$
89 | {cart?.total / 100 - cart?.discount_total / 100}
90 |
91 |
92 |
93 | {/* A button to navigate to checkout screen */}
94 |
104 |
105 |
106 | );
107 | }
108 |
109 | // Styles....
110 | const styles = StyleSheet.create({
111 | container: {
112 | flex: 1,
113 | backgroundColor: "#fff",
114 | alignItems: "center",
115 | },
116 | row: {
117 | flexDirection: "row",
118 | justifyContent: "space-between",
119 | width: widthToDp(90),
120 | marginTop: 10,
121 | },
122 | total: {
123 | borderTopWidth: 1,
124 | paddingTop: 10,
125 | borderTopColor: "#E5E5E5",
126 | marginBottom: 10,
127 | },
128 | cartTotalText: {
129 | fontSize: widthToDp(4.5),
130 | color: "#989899",
131 | },
132 | });
133 |
--------------------------------------------------------------------------------
/screens/Checkout.js:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React, { useEffect, useState } from "react";
3 | import Header from "../components/Header";
4 | import axios from "axios";
5 | import baseURL from "../constants/url";
6 | import { ScrollView } from "react-native-gesture-handler";
7 | import { SafeAreaView } from "react-native-safe-area-context";
8 | import { heightToDp, widthToDp } from "rn-responsive-screen";
9 | import Button from "../components/Button";
10 | import ShippingAddress from "../components/ShippingAddress";
11 | import Payment from "../components/Payment";
12 | import RadioButton from "../components/RadioButton";
13 | import { secret_key, publishable_key } from "../constants/stripe";
14 | import { CardField, useStripe } from "@stripe/stripe-react-native";
15 | import AsyncStorage from "@react-native-async-storage/async-storage";
16 | import { Actions } from "react-native-router-flux";
17 |
18 | export default function Checkout({ cart }) {
19 | const [paymentInfo, setPaymentInfo] = useState({});
20 | const [shippingAddress, setShippingAddress] = useState({});
21 | const [shippingOptions, setShippingOptions] = useState([]);
22 | const [selectedShippingOption, setSelectedShippingOption] = useState("");
23 | const [paymentSession, setPaymentSession] = useState({});
24 |
25 | const { confirmPayment } = useStripe();
26 |
27 | const handlePaymentInputChange = (card) => {
28 | setPaymentInfo(card);
29 | };
30 |
31 | const handleAddressInputChange = (address) => {
32 | setShippingAddress(address);
33 | };
34 |
35 | const handlePayment = async () => {
36 | // Getting client secret from the payment session state
37 | const clientSecret = paymentSession.data
38 | ? paymentSession.data.client_secret
39 | : paymentSession.client_secret;
40 |
41 | const billingDetails = {
42 | email: shippingAddress.email,
43 | phone: shippingAddress.phone,
44 | addressCity: shippingAddress.city,
45 | addressCountry: shippingAddress.country,
46 | addressLine1: shippingAddress.address_1,
47 | addressLine2: shippingAddress.address_2,
48 | addressPostalCode: shippingAddress.postalCode,
49 | };
50 | const { error, paymentIntent } = await confirmPayment(clientSecret, {
51 | type: "Card",
52 | billingDetails,
53 | });
54 | if (error) {
55 | alert("Payment failed", error);
56 | }
57 | if (paymentIntent) {
58 | alert("Payment successful");
59 | // Calling the complete cart function to empty the cart and redirect to the home screen
60 | completeCart();
61 | }
62 | };
63 |
64 | const completeCart = async () => {
65 | const cartId = await AsyncStorage.getItem("cart_id");
66 |
67 | // Sending a request to the server to empty the cart
68 | axios
69 | .post(`${baseURL}/store/carts/${cartId}/complete`)
70 | .then(async (res) => {
71 | // Removing the cart_id from the local storage
72 | await AsyncStorage.removeItem("cart_id");
73 | // Redirecting to the home screen
74 | Actions.push("products");
75 | });
76 | };
77 |
78 | // Calling the API when user presses the "Place Order" button
79 | const placeOrder = async () => {
80 | // Getting cart id from async storage
81 | let cart_id = await AsyncStorage.getItem("cart_id");
82 | // Post shipping address to server
83 | axios
84 | .post(`${baseURL}/store/carts/${cart_id}`, {
85 | shipping_address: shippingAddress,
86 | })
87 | .then(({ data }) => {
88 | // Post shipping method to server
89 | axios
90 | .post(`${baseURL}/store/carts/${cart_id}/shipping-methods`, {
91 | option_id: selectedShippingOption,
92 | })
93 | .then(({ data }) => {
94 | // Calling the handle Payment API
95 | handlePayment();
96 | });
97 | });
98 | };
99 |
100 | const fetchPaymentOption = async () => {
101 | // Getting cart id from async storage
102 | let cart_id = await AsyncStorage.getItem("cart_id");
103 |
104 | // Fetch shipping options from server
105 | axios
106 | .get(`${baseURL}/store/shipping-options/${cart_id}`)
107 | .then(({ data }) => {
108 | setShippingOptions(data.shipping_options);
109 | // Initializing payment session
110 | InitializePaymentSessions();
111 | });
112 | };
113 |
114 | const InitializePaymentSessions = async () => {
115 | // Getting cart id from async storage
116 | let cart_id = await AsyncStorage.getItem("cart_id");
117 | // Intializing payment session
118 | axios
119 | .post(`${baseURL}/store/carts/${cart_id}/payment-sessions`)
120 | .then(({ data }) => {
121 | axios
122 | .post(`${baseURL}/store/carts/${cart_id}/payment-session`, {
123 | provider_id: "stripe",
124 | })
125 | .then(({ data }) => {
126 | console.log("data =>", data.cart.payment_session);
127 | setPaymentSession(data.cart.payment_session);
128 | });
129 | });
130 | };
131 |
132 | useEffect(() => {
133 | // Calling the function to fetch the payment options when the component mounts
134 | fetchPaymentOption();
135 | }, []);
136 | return (
137 |
138 |
139 |
140 |
141 | Shipping Address
142 |
143 |
144 |
145 |
146 | Payment
147 | {
162 | handlePaymentInputChange(cardDetails);
163 | }}
164 | onFocus={(focusedField) => {
165 | console.log("focusField", focusedField);
166 | }}
167 | />
168 |
169 |
170 | Shipping Options
171 | {shippingOptions.map((option) => (
172 |
173 | setSelectedShippingOption(option.id)}
175 | key={option.id}
176 | selected={selectedShippingOption === option.id}
177 | children={option.name}
178 | />
179 |
180 | ))}
181 |
182 |
183 |
184 |
185 |
186 | );
187 | }
188 |
189 | const styles = StyleSheet.create({
190 | container: {
191 | flex: 1,
192 | backgroundColor: "#fff",
193 | },
194 | address: {
195 | marginHorizontal: widthToDp(5),
196 | },
197 | payment: {
198 | marginHorizontal: widthToDp(5),
199 | marginTop: heightToDp(4),
200 | },
201 | shipping: {
202 | marginHorizontal: widthToDp(5),
203 | },
204 | title: {
205 | fontSize: widthToDp(4.5),
206 | },
207 | shippingOption: {
208 | marginTop: heightToDp(2),
209 | },
210 | });
211 |
--------------------------------------------------------------------------------
/screens/ProductInfo.js:
--------------------------------------------------------------------------------
1 | import {
2 | View,
3 | Text,
4 | ScrollView,
5 | TouchableOpacity,
6 | StyleSheet,
7 | } from "react-native";
8 | import React, { useState, useEffect } from "react";
9 | import axios from "axios";
10 | import { SafeAreaView } from "react-native-safe-area-context";
11 | import Images from "../components/ProductInfo/Image";
12 | import baseURL from "../constants/url";
13 | import { Actions } from "react-native-router-flux";
14 | import { Ionicons } from "@expo/vector-icons";
15 | import MetaInfo from "../components/ProductInfo/MetaInfo";
16 |
17 | export default function ProductInfo({ productId }) {
18 | const [productInfo, setproductInfo] = useState(null);
19 |
20 | useEffect(() => {
21 | axios.get(`${baseURL}/store/products/${productId}`).then((res) => {
22 | setproductInfo(res.data.product);
23 | });
24 | }, []);
25 |
26 | return (
27 |
28 | Actions.pop()}>
29 |
35 |
36 |
37 | {productInfo && (
38 |
39 |
40 |
41 |
42 | )}
43 |
44 |
45 | );
46 | }
47 |
48 | const styles = StyleSheet.create({
49 | container: {
50 | flex: 1,
51 | backgroundColor: "#fff",
52 | justifyContent: "center",
53 | },
54 | icon: {
55 | marginLeft: 10,
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/screens/Products.js:
--------------------------------------------------------------------------------
1 | import { ScrollView, StyleSheet, TouchableOpacity, View } from "react-native";
2 | import React, { useEffect, useState } from "react";
3 | import ProductCard from "../components/ProductCard";
4 | import { widthToDp } from "rn-responsive-screen";
5 | import axios from "axios";
6 | import Header from "../components/Header";
7 | import { Actions } from "react-native-router-flux";
8 | import baseURL from "../constants/url";
9 | import Button from "../components/Button";
10 | import { Feather } from "@expo/vector-icons";
11 |
12 | export default function Products() {
13 | const [products, setProducts] = useState([]);
14 |
15 | function fetchProducts() {
16 | axios.get(`${baseURL}/store/products`).then((res) => {
17 | setProducts(res.data.products);
18 | });
19 | }
20 |
21 | useEffect(() => {
22 | fetchProducts();
23 | }, []);
24 |
25 | return (
26 |
27 |
28 |
29 |
30 | {products.map((product) => (
31 | Actions.ProductInfo({ productId: product.id })}
34 | >
35 |
36 |
37 | ))}
38 |
39 |
40 |
41 | Actions.cart()}
46 | />
47 |
48 |
49 | );
50 | }
51 |
52 | const styles = StyleSheet.create({
53 | container: {
54 | flex: 1,
55 | paddingTop: 50,
56 | backgroundColor: "#fff",
57 | alignItems: "center",
58 | justifyContent: "center",
59 | },
60 | products: {
61 | flex: 1,
62 | flexDirection: "row",
63 | flexWrap: "wrap",
64 | width: widthToDp(100),
65 | paddingHorizontal: widthToDp(4),
66 | justifyContent: "space-between",
67 | },
68 | addToCart: {
69 | position: "absolute",
70 | bottom: 30,
71 | right: 10,
72 | backgroundColor: "#C37AFF",
73 | width: widthToDp(12),
74 | height: widthToDp(12),
75 | borderRadius: widthToDp(10),
76 | alignItems: "center",
77 | padding: widthToDp(2),
78 | justifyContent: "center",
79 | },
80 | });
81 |
--------------------------------------------------------------------------------