В настоящее время Medusa проводит Хакатон, где разработчики могут создавать удивительные проекты с Medusa и выигрывать призы и подарки. Примером проекта хакатона является создание приложения для электронной коммерции с помощью React Native, которое рассматривается в этой статье.

Это вторая часть серии статей Создание приложения электронной коммерции React Native с помощью Medusa. В предыдущей статье вы узнали, как настроить сервер Medusa и администратора, а также как создать базовое приложение для электронной коммерции.

В этой части вы узнаете, как реализовать функции корзины и оформления заказа. Вы также можете найти исходный код приложения React Native на GitHub.

Предпосылки

  • Вам нужна учетная запись Stripe, вы можете зарегистрироваться здесь.

Клонировать стартовый код

Если вы пропустили часть 1, вы можете клонировать код с GitHub. Для этого просто запустите следующую команду:

git clone -b part-1 https://github.com/suhailkakar/react-native-medusajs

После того, как вы клонировали репозиторий, перейдите во вновь созданный каталог react-native-medusajs, запустив cd react-native-medusajs. Запустите следующую команду, чтобы установить модули узла:

yarn install

Приведенная выше команда установит необходимые пакеты и зависимости из реестра NPM.

После успешной установки зависимостей запустите expo start, чтобы запустить приложение. Вы можете отсканировать QR-код с помощью своего устройства или запустить приложение на симуляторе Android/iOS. Вы должны увидеть аналогичный экран, как только приложение откроется на вашем устройстве/эмуляторе.

Убедитесь, что сервер Medusa также работает.

Возможно, вам придется изменить URL-адрес в constants/urls.js, если вы не видите продукты. Вы можете обновить базовый URL-адрес до IP-адреса, на котором работает внутренний сервер.

Создание корзины

Первым шагом является реализация функциональности корзины. Установите async-storage, чтобы сохранить идентификатор корзины в локальном хранилище устройства. Запустите следующую команду, чтобы установить @react-native-async-storage/async-storage в проект:

expo install @react-native-async-storage/async-storage

После установки пакета откройте проект в любом редакторе кода и вставьте приведенный ниже код в App.js перед функцией возврата:

const getCartId = () => {
    axios.post(`${baseURL}/store/carts`).then((res) => {
      AsyncStorage.setItem("cart_id", res.data.cart.id);
    });
  };

В приведенном выше коде вы создали функцию с именем getCartId, и внутри этой функции вы запрашиваете у внутреннего API-интерфейса получение идентификатора корзины с помощью Axios, а затем сохраняете его в локальном хранилище устройства с помощью async-storage.

Обязательно импортируйте async-storage, useEffect, Axios и baseURL в файл App.js:

import axios from "axios";
import baseURL from "./constants/url";
import { useEffect } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";

Вышеупомянутая функция (getCartId) не будет запущена, так как вы еще не вызвали ее. Однако вы хотите запускать эту функцию только в том случае, если в локальном хранилище устройства нет сохраненного идентификатора корзины.

Теперь напишите еще одну функцию, чтобы проверить, есть ли уже идентификатор корзины в локальном хранилище устройства. Если нет, функция должна вызвать getCartId. Наконец, вызовите checkCartId внутри useEffect:

// Check cart_id 
  const checkCartId = async () => {
    const cartId = await AsyncStorage.getItem("cart_id");
    if (!cartId) {
      getCartId();
    }
  };
useEffect(() => {
    checkCartId();
}, []);

Добавление товаров в корзину

Во-первых, давайте обновим компонент кнопки и добавим свойство для обновления макета кнопки в зависимости от предоставленного размера. Вы можете заменить код внутри components/Button.js кодом ниже:

import { View, Text, StyleSheet } from "react-native";
import React from "react";
import { widthToDp } from "rn-responsive-screen";
export default function Button({ title, onPress, style, textSize, large }) {
  return (
    <View style={[styles.container, style, large && styles.large]}>
      <Text
        style={[
          styles.text,
          { fontSize: textSize ? textSize : widthToDp(3.5) },
          ,
        ]}
        onPress={onPress}
      >
        {title}
      </Text>
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    backgroundColor: "#C37AFF",
    padding: 5,
    width: widthToDp(20),
    alignItems: "center",
    justifyContent: "center",
    borderRadius: 59,
  },
  large: {
    width: "100%",
    marginTop: 10,
    height: widthToDp(12),
  },
  text: {
    color: "#fff",
    fontWeight: "bold",
  },
});

Вспомните, что к концу части 1 вы создали компонент components/ProductInfo/MetaInfo.js для отображения описания, размера и другой информации о товаре. Идем дальше и добавляем компонент кнопки после описания:

<Button title="Add to Cart" onPress={addToCart} large={true} />

Также не забудьте импортировать компонент кнопки из components/Button:

import Button from "../Button";

Как видите, вы вызываете функцию addToCart, когда пользователь нажимает кнопку, поэтому объявите эту функцию.

Поверх компонента MetaInfo.js перед возвратом объявите новую функцию с именем addToCart и добавьте в нее следующий код:

const addToCart = async () => {
    const cartId = await AsyncStorage.getItem("cart_id");
    axios
      .post(`${baseURL}/store/carts/${cartId}/line-items`, {
        variant_id: product.variants[0].id,
        quantity: 1,
      })
      .then(({ data }) => {
        alert(`Item ${product.title} added to cart`);
      })
      .catch((err) => {
        console.log(err);
      });
  };

В приведенном выше коде сначала вы получаете cart_id из асинхронного хранилища, а затем публикуете первый вариант продукта с 1 количеством в API корзины. Затем вы направляете пользователей на экран корзины.

Вам также необходимо импортировать AsyncStorage, axios и baseURL в файл MetaInfo.js:

import axios from "axios";
import baseURL from "../../constants/url";
import AsyncStorage from "@react-native-async-storage/async-storage";

Экран корзины

Это экран, на котором пользователь увидит товары, добавленные в корзину. Создайте новый файл с именем screens/Cart.js и используйте его для рендеринга простого компонента Text:

import { View, Text } from "react-native";
import React from "react";
export default function Cart() {
  return (
    <View>
      <Text>Cart Screen</Text>
    </View>
  );
}

Затем импортируйте экран корзины вверху App.js:

import Cart from "./screens/Cart";

Добавьте новый компонент Scene под существующим компонентом Scene в возвращенном JSX:

<Scene key="cart" component={Cart} hideNavBar />

В каталоге components создайте новый файл с именем CartItem.js и добавьте следующее содержимое:

import { View, Text, StyleSheet, Image } from "react-native";
import React from "react";
import { heightToDp, width, widthToDp } from "rn-responsive-screen";
export default function CartItem({ product }) {
  return (
    <View style={styles.container}>
      <Image source={{ uri: product.thumbnail }} style={styles.image} />
      <View style={styles.info}>
        <View>
          <Text style={styles.title}>{product.title}</Text>
          <Text style={styles.description}>
            {product.description} • ${product.unit_price / 100}
          </Text>
        </View>
        <View style={styles.footer}>
          <Text style={styles.price}>${product.total / 100}</Text>
          <Text style={styles.quantity}>x{product.quantity}</Text>
        </View>
      </View>
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    marginTop: 20,
    flexDirection: "row",
    borderBottomWidth: 1,
    paddingBottom: 10,
    borderColor: "#e6e6e6",
    width: widthToDp("90%"),
  },
  image: {
    width: widthToDp(30),
    height: heightToDp(30),
    borderRadius: 10,
  },
  title: {
    fontSize: widthToDp(4),
    fontWeight: "bold",
  },
  footer: {
    flexDirection: "row",
    justifyContent: "space-between",
  },
  info: {
    marginLeft: widthToDp(3),
    flexDirection: "column",
    justifyContent: "space-between",
    marginVertical: heightToDp(2),
    width: widthToDp(50),
  },
  description: {
    fontSize: widthToDp(3.5),
    color: "#8e8e93",
    marginTop: heightToDp(2),
  },
  price: {
    fontSize: widthToDp(4),
  },
  quantity: {
    fontSize: widthToDp(4),
  },
});

Приведенный выше код представляет собой простой компонент корзины React Native, который будет отображаться на экране корзины. Вы передаете реквизиты компоненту из родительского компонента. Это компонент, который будет представлять товар в корзине.

Затем в файле screens/Cart.js добавьте приведенную ниже функцию и useState перед оператором return, чтобы получить продукты из API корзины и сохранить их в состоянии:

const [cart, setCart] = useState([]);
  const fetchCart = async () => {
    // Get the cart id from the device storage
    const cartId = await AsyncStorage.getItem("cart_id");
    // Fetch the products from the cart API using the cart id
    axios.get(`${baseURL}/store/carts/${cartId}`).then(({ data }) => {
      // Set the cart state to the products in the cart
      setCart(data.cart);
    });
  };
  useEffect(() => {
        // Calling the fetchCart function when the component mounts
    fetchCart();
  }, []);

В приведенном выше коде вы получаете идентификатор корзины из хранилища устройства, извлекаете продукты из API корзины и сохраняете данные в состояние. Затем вы вызываете функцию внутри useEffect. Замените импорт в верхней части файла screens/Cart.js кодом ниже:

import { View, Text, StyleSheet } from "react-native";
import React, { useEffect, useState } from "react";
import Header from "../components/Header";
import axios from "axios";
import baseURL from "../constants/url";
import CartItem from "../components/CartItem";
import { ScrollView } from "react-native-gesture-handler";
import { SafeAreaView } from "react-native-safe-area-context";
import { width, widthToDp } from "rn-responsive-screen";
import Button from "../components/Button";
import { Actions } from "react-native-router-flux";
import AsyncStorage from "@react-native-async-storage/async-storage";

Теперь, когда вы получили данные и сохранили их в состоянии, пришло время отобразить продукты на экране корзины. Замените все ниже функции useEffect на следующий код:

return (
    // SafeAreaView is used to avoid the notch on the phone
    <SafeAreaView style={[styles.container]}>
      {/* SchrollView is used in order to scroll the content */}
      <ScrollView contentContainerStyle={styles.container}>
        {/* Using the reusable header component */}
        <Header title="My Cart" />
        {/* Mapping the products into the Cart component */}
        {cart?.items?.map((product) => (
          <CartItem product={product} />
        ))}
      </ScrollView>
      {/* Creating a seperate view to show the total amount and checkout button */}
      <View>
        <View style={styles.row}>
          <Text style={styles.cartTotalText}>Items</Text>
          {/* Showing Cart Total */}
          <Text
            style={[
              styles.cartTotalText,
              {
                color: "#4C4C4C",
              },
            ]}
          >
            {/* Dividing the total by 100 because Medusa doesn't store numbers in decimal */}
            ${cart?.total / 100}
          </Text>
        </View>
        <View style={styles.row}>
          {/* Showing the discount (if any) */}
          <Text style={styles.cartTotalText}>Discount</Text>
          <Text
            style={[
              styles.cartTotalText,
              {
                color: "#4C4C4C",
              },
            ]}
          >
            - ${cart?.discount_total / 100}
          </Text>
        </View>
        <View style={[styles.row, styles.total]}>
          <Text style={styles.cartTotalText}>Total</Text>
          <Text
            style={[
              styles.cartTotalText,
              {
                color: "#4C4C4C",
              },
            ]}
          >
            {/* Calculating the total */}$
            {cart?.total / 100 - cart?.discount_total / 100}
          </Text>
        </View>
        <View>
          {/* A button to navigate to checkout screen */}
          <Button
            large={true}
            onPress={() => {
              Actions.checkout({
                cart,
              });
            }}
            title={cart?.items?.length > 0 ? "Checkout" : "Empty Cart"}
          />
        </View>
      </View>
    </SafeAreaView>
  );
}
// Styles....
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
  },
  row: {
    flexDirection: "row",
    justifyContent: "space-between",
    width: widthToDp(90),
    marginTop: 10,
  },
  total: {
    borderTopWidth: 1,
    paddingTop: 10,
    borderTopColor: "#E5E5E5",
    marginBottom: 10,
  },
  cartTotalText: {
    fontSize: widthToDp(4.5),
    color: "#989899",
  },
});

Затем вы добавите базовый и простой компонент кнопки в screens/Products.js, чтобы перейти к экрану корзины.

Добавьте приведенный ниже код после компонента ScrollView на экране screens/Products.js:

<View style={styles.addToCart}>
        <Feather
          name="shopping-cart"
          size={24}
          color="white"
          onPress={() => Actions.cart()}
        />
      </View>

Не забудьте также добавить объект стиля внутри функции StyleSheet.create:

addToCart: {
    position: "absolute",
    bottom: 30,
    right: 10,
    backgroundColor: "#C37AFF",
    width: widthToDp(12),
    height: widthToDp(12),
    borderRadius: widthToDp(10),
    alignItems: "center",
    padding: widthToDp(2),
    justifyContent: "center",
  },

Вам также необходимо импортировать значки перьев из @expo/vector-icons вверху файла:

import { Feather } from "@expo/vector-icons";

Экран тестовой корзины

Экран корзины готов. Убедитесь, что сервер Medusa запущен, затем перезапустите приложение React Native.

Попробуйте добавить несколько товаров в корзину, а затем просмотреть корзину. Экран вашей корзины должен выглядеть так:

Оформление заказа и оплата

Теперь, когда мы завершили процесс оформления корзины, пришло время оформления заказа.

Настройте плагин Stripe

Перейдите в каталог сервера Medusa и установите плагин Stripe, выполнив следующую команду:

npm install medusa-payment-stripe

Добавьте следующий код в конец массива плагинов в файле medusa-config.js.

const plugins = [
  ...,
  {
    resolve: `medusa-payment-stripe`,
    options: {
      api_key: process.env.STRIPE_API_KEY,
    },
  },
];

В этом уроке я использовал Stripe для обработки платежей. Перейдите на панель управления Stripe и включите переключатель Тестовый режим в правом верхнем углу. Нажмите кнопку разработчиков рядом с тестовым режимом. Слева выберите Ключ API, и вы увидите Ключ для публикации и Секретный ключ. Скопируйте два ключа, так как они понадобятся вам позже.

В файле .env вставьте свой секретный ключ, где написано STRIPE_API_KEY.

Включить Stripe в качестве поставщика платежей

Убедитесь, что сервер Medusa и панель администратора запущены. Теперь откройте панель администратора Medusa и выберите Настройки на боковой панели. Затем выберите Регионы.

Затем выберите регионы, в которые вы хотите добавить Stripe в качестве поставщика платежей. В настройках справа нажмите на значок с тремя точками и выберите «Редактировать сведения о регионе».

В открывшемся новом окне выберите Stripe в поле поставщиков платежных услуг. Закончив, нажмите кнопку Сохранить и закрыть.

Экран оформления заказа

Вернувшись в приложение React Native, создайте новый файл screens/Checkout.js и добавьте в него простой компонент Text:

import { View, Text } from "react-native";
import React from "react";
export default function Checkout() {
  return (
    <View>
      <Text>Checkout</Text>
    </View>
  );
}

Затем импортируйте экран оформления заказа в верхней части App.js:

import Checkout from "./screens/Checkout";

Добавьте новый компонент Scene под существующими компонентами Scene в возвращенном JSX:

<Scene key="checkout" component={Checkout} hideNavBar />

В папке компонентов создайте новый файл с именем ShippingAddress.js и добавьте в него следующий код:

// Importing a few package and components
import { View, StyleSheet, Text, TextInput } from "react-native";
import React, { useState } from "react";
import { heightToDp } from "rn-responsive-screen";
export default function ShippingAddress({ onChange }) {
  // Passing onChange as a prop
  // Declaring a few states to store the user's input
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [AddressLine1, setAddressLine1] = useState("");
  const [AddressLine2, setAddressLine2] = useState("");
  const [city, setCity] = useState("");
  const [country, setCountry] = useState("");
  const [province, setProvince] = useState("");
  const [postalCode, setPostalCode] = useState("");
  const [phone, setPhone] = useState("");
  const [company, setCompany] = useState("");
  const [email, setEmail] = useState("");
  // Function to handle the user's input
  const handleChange = () => {
    // Creating an object to store the user's input
    let address = {
      first_name: firstName,
      last_name: lastName,
            email,
      address_1: AddressLine1,
      address_2: AddressLine2,
      city,
      province,
      postal_code: postalCode,
      phone,
      company,
            country
    };
    // Calling the onChange function and passing the address object as an argument
    onChange(address);
  };
  return (
    // Creating a view to hold the user's input
    <View style={styles.container}>
      {/* Creating a text input for the user's first name */}
      <TextInput
        onChangeText={(e) => {
          // Setting the user's input to the firstName state
          setFirstName(e);
          // Calling the handleChange function
          handleChange();
        }}
        placeholder="First Name"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setLastName(e);
          handleChange();
        }}
        placeholder="Last Name"
        style={styles.input}
      />
         <TextInput
        onChangeText={(e) => {
          setEmail(e);
          handleChange();
        }}
        placeholder="Email"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setAddressLine1(e);
          handleChange();
        }}
        placeholder="Address Line 1"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setAddressLine2(e);
          handleChange();
        }}
        placeholder="Address Line 2"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setCity(e);
          handleChange();
        }}
        placeholder="City"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setCountry(e);
          handleChange();
        }}
        placeholder="Country"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setProvince(e);
          handleChange();
        }}
        placeholder="Province"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setPostalCode(e);
          handleChange();
        }}
        placeholder="Postal Code"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setPhone(e);
          handleChange();
        }}
        placeholder="Phone"
        style={styles.input}
      />
      <TextInput
        onChangeText={(e) => {
          setCompany(e);
          handleChange();
        }}
        placeholder="Company"
        style={styles.input}
      />
    </View>
  );
}
// Creating a stylesheet to style the view
const styles = StyleSheet.create({
  container: {
    marginTop: heightToDp(2),
  },
  input: {
    borderWidth: 1,
    padding: 12,
    borderColor: "#E5E5E5",
    borderRadius: 5,
    marginTop: 10.2,
  },
});

В приведенном выше коде вы импортировали несколько компонентов и создали функцию, которая обрабатывает событие onChange для входных данных. Вы также создали несколько TextInputs для адреса доставки.

Создайте новый файл с именем RadioButton.js в папке компонентов и добавьте в него приведенный ниже код. Это очень простой компонент, который будет отображать переключатели:

import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import React from "react";
const RadioButton = ({ onPress, selected, children }) => {
  return (
    <View style={styles.radioButtonContainer}>
      <TouchableOpacity onPress={onPress} style={styles.radioButton}>
        {selected ? <View style={styles.radioButtonIcon} /> : null}
      </TouchableOpacity>
      <TouchableOpacity onPress={onPress}>
        <Text style={styles.radioButtonText}>{children}</Text>
      </TouchableOpacity>
    </View>
  );
};
const styles = StyleSheet.create({
  radioButtonContainer: {
    flexDirection: "row",
    alignItems: "center",
    marginRight: 45,
  },
  radioButton: {
    height: 20,
    width: 20,
    backgroundColor: "#F8F8F8",
    borderRadius: 10,
    borderWidth: 1,
    borderColor: "#E6E6E6",
    alignItems: "center",
    justifyContent: "center",
  },
  radioButtonIcon: {
    height: 14,
    width: 14,
    borderRadius: 7,
    backgroundColor: "#C37AFF",
  },
  radioButtonText: {
    fontSize: 16,
    marginLeft: 16,
  },
});
export default RadioButton;

Создайте еще один файл с именем stripe.js в папке с константами и добавьте приведенный ниже код. Обязательно обновите YOUR_STRIPE_PUBLISHABLE_KEY своим публикуемым ключом:

const publishable_key = "YOUR_STRIPE_PUBLISHABLE_KEY";
export { publishable_key };

Затем установите Stripe SDK для React Native, используя следующую команду:

npm install @stripe/stripe-react-native

Теперь обновите экран проверки (screens/Checkout.js), заменив импорт следующими компонентами и зависимостями:

import { View, Text, StyleSheet } from "react-native";
import React, { useEffect, useState } from "react";
import Header from "../components/Header";
import axios from "axios";
import baseURL from "../constants/url";
import { ScrollView } from "react-native-gesture-handler";
import { SafeAreaView } from "react-native-safe-area-context";
import { heightToDp, widthToDp } from "rn-responsive-screen";
import Button from "../components/Button";
import ShippingAddress from "../components/ShippingAddress";
import Payment from "../components/Payment";
import { publishable_key } from "../constants/stripe";
import RadioButton from "../components/RadioButton";
import { CardField, useStripe } from "@stripe/stripe-react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { StripeProvider } from "@stripe/stripe-react-native";

Затем создайте несколько useStates, которые будут использоваться для захвата входных значений в компоненте Checkout:

const [paymentInfo, setPaymentInfo] = useState({});
const [shippingAddress, setShippingAddress] = useState({});
const [shippingOptions, setShippingOptions] = useState([]);
const [selectedShippingOption, setSelectedShippingOption] = useState("");
const [paymentSession, setPaymentSession] = useState({});
const { confirmPayment } = useStripe();

Создайте две функции, которые будут использоваться для захвата входных значений, которые мы объявим позже.

// const [selectedShippingOption, setSelectedShippingOption] = useState("");
// ......
  const handlePaymentInputChange = (card) => {
    setPaymentInfo(card.values);
  };
  const handleAddressInputChange = (address) => {
    setShippingAddress(address);
  };

При открытии страницы и до того, как платежные системы будут показаны клиенту на выбор, необходимо инициализировать платежные сессии. Для этого мы создаем функцию с именем InitializePaymentSessions и вызываем ее в функции useEffect. Вот как должен выглядеть код:

const InitializePaymentSessions = async () => {
    // Getting cart id from async storage
    let cart_id = await AsyncStorage.getItem("cart_id");
    // Intializing payment session
    axios
      .post(`${baseURL}/store/carts/${cart_id}/payment-sessions`)
      .then(({ data }) => {
        axios
          .post(`${baseURL}/store/carts/${cart_id}/payment-session`, {
            provider_id: "stripe",
          })
          .then(({ data }) => {
            setPaymentSession(data.cart.payment_session);
          });
      });
  };
  useEffect(() => {
    // Calling the function to fetch the payment options when the component mounts
    fetchPaymentOption();
  }, []);

Затем создайте другие функции для обработки платежей. Добавьте следующие функции Checkout.js :

const handlePayment = async () => {
        // Getting client secret from the payment session state
    const clientSecret = paymentSession.data ? paymentSession.data.client_secret : paymentSession.client_secret
    const billingDetails = {
      email: shippingAddress.email,
      phone: shippingAddress.phone,
      addressCity: shippingAddress.city,
      addressCountry: shippingAddress.country,
      addressLine1: shippingAddress.address_1,
      addressLine2: shippingAddress.address_2,
      addressPostalCode: shippingAddress.postalCode,
    };
    const { error, paymentIntent } = await confirmPayment(clientSecret, {
      type: "Card",
      billingDetails,
    });
        if (error) {
      alert("Payment failed", error);
    }
    if (paymentIntent) {
      alert("Payment successful");
// Calling the complete cart function to empty the cart and redirect to the home screen
    completeCart();
    }
  };
const completeCart = async () => {
    const cartId = await AsyncStorage.getItem("cart_id");
    // Sending a request to the server to empty the cart
    axios
      .post(`${baseURL}/store/carts/${cartId}/complete`)
      .then(async (res) => {
        // Removing the cart_id from the local storage
        await AsyncStorage.removeItem("cart_id");
        // Redirecting to the home screen
        Actions.push("products");
      });
  };
  // Calling the API when user presses the "Place Order" button
  const placeOrder = async () => {
    // Getting cart id from async storage
    let cart_id = await AsyncStorage.getItem("cart_id");
    // Post shipping address to server
    axios
      .post(`${baseURL}/store/carts/${cart_id}`, {
        shipping_address: shippingAddress,
      })
      .then(({ data }) => {
        // Post shipping method to server
        axios
          .post(`${baseURL}/store/carts/${cart_id}/shipping-methods`, {
            option_id: selectedShippingOption,
          })
          .then(({ data }) => {
            // Calling the handle Payment API
            handlePayment();
          });
      });
  };
  const fetchPaymentOption = async () => {
    // Getting cart id from async storage
    let cart_id = await AsyncStorage.getItem("cart_id");
    // Fetch shipping options from server
    axios
      .get(`${baseURL}/store/shipping-options/${cart_id}`)
      .then(({ data }) => {
        setShippingOptions(data.shipping_options);
        // Initializing payment session
        InitializePaymentSessions();
      });
  };

Эти функции позволяют получать варианты оплаты и доставки от Medusa. Он также обрабатывает платеж через Stripe, а затем обрабатывает оплату в Medusa.

Вы также можете заменить функцию возврата кодом ниже:

return (
<StripeProvider publishableKey={publishable_key}>
    <SafeAreaView style={styles.container}>
      <ScrollView>
        <Header title="Checkout" />
        <View style={styles.address}>
          <Text style={styles.title}>Shipping Address</Text>
          <ShippingAddress onChange={handleAddressInputChange} />
        </View>
        <View style={styles.payment}>
          <Text style={styles.title}>Payment</Text>
          <CardField
            postalCodeEnabled={false}
            placeholders={{
              number: "4242 4242 4242 4242",
            }}
            cardStyle={{
              backgroundColor: "#FFFFFF",
              textColor: "#000000",
            }}
            style={{
              width: "100%",
              height: 50,
              marginVertical: 30,
            }}
            onCardChange={(cardDetails) => {
              handlePaymentInputChange(cardDetails);
            }}
            onFocus={(focusedField) => {
              console.log("focusField", focusedField);
            }}
          />
        </View>
        <View style={styles.shipping}>
          <Text style={styles.title}>Shipping Options</Text>
          {shippingOptions.map((option) => (
            <View style={styles.shippingOption}>
              <RadioButton
                onPress={() => setSelectedShippingOption(option.id)}
                key={option.id}
                selected={selectedShippingOption === option.id}
                children={option.name}
              />
            </View>
          ))}
          <Button onPress={placeOrder} large title="Place Order" />
        </View>
      </ScrollView>
    </SafeAreaView>
</StripeProvider>
  );

Наконец, добавьте следующий объект стиля в конец файла:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
  },
  address: {
    marginHorizontal: widthToDp(5),
  },
  payment: {
    marginHorizontal: widthToDp(5),
    marginTop: heightToDp(4),
  },
  shipping: {
    marginHorizontal: widthToDp(5),
  },
  title: {
    fontSize: widthToDp(4.5),
  },
  shippingOption: {
    marginTop: heightToDp(2),
  },
});

Вот и все! Теперь вы сможете совершать платежи и покупать товары в магазине.

Тестовый экран проверки

Теперь попробуйте добавить несколько товаров в корзину и перейти к экрану оформления заказа, вы сможете добавить адрес и произвести оплату. Вот как должен выглядеть экран оформления заказа:

Что дальше?

Эта статья является частью 2 статьи Создание приложения для электронной коммерции с помощью Medusa и React Native. Вы также можете добавить дополнительные функции с помощью Medusa:

  1. Добавьте поисковую систему с помощью MeiliSearch.
  2. Ознакомьтесь с документацией, чтобы узнать, что еще можно сделать с Medusa.

Если у вас есть какие-либо проблемы или вопросы, связанные с Medusa, не стесняйтесь обращаться к команде Medusa через Discord.