使用 React Native Expo 实施条带支付时出错

Error when implementing stripe payment with react native expo

我在尝试使用 React Native 和 Expo SDK 实现 Stripe 支付时遇到错误。 场景非常简单,我将商品添加到购物车,然后选择支付选项作为支付卡,但是当我点击卡时,出现错误。错误和代码如下。

import { StatusBar } from "expo-status-bar";
import React from "react";
import {
  StyleSheet,
  Text,
  View,
  Button,
  Pressable,
  Platform,
} from "react-native";
import { StripeProvider } from "@stripe/stripe-react-native";
import { initStripe, useStripe } from "@stripe/stripe-react-native";
import GooglePayMark from "./GooglePayMark";
import ApplePayMark from "./ApplePayMark";
const API_URL = "http://192.168.0.163:3000";
const ProductRow = ({ product, cart, setCart }) => {
  const modifyCart = (delta) => {
    setCart({ ...cart, [product.id]: cart[product.id] + delta });
  };
  return (
    <View style={styles.productRow}>
      <View style={{ flexDirection: "row" }}>
        <Text style={{ fontSize: 17, flexGrow: 1 }}>
          {product.name} - {product.price}$
        </Text>
        <Text style={{ fontSize: 17, fontWeight: "700" }}>
          {cart[product.id]}
        </Text>
      </View>
      <View
        style={{
          flexDirection: "row",
          justifyContent: "space-between",
          marginTop: 8,
        }}
      >
        <Button
          disabled={cart[product.id] <= 0}
          title="Remove"
          onPress={() => modifyCart(-1)}
        />
        <Button title="Add" onPress={() => modifyCart(1)} />
      </View>
    </View>
  );
};

const ProductsScreen = ({ products, navigateToCheckout }) => {
  /**
   * We will save the state of the cart here
   * It will have the inital shape:
   * {
   *  [product.id]: 0
   * }
   */
  const [cart, setCart] = React.useState(
    Object.fromEntries(products.map((p) => [p.id, 0]))
  );

  const handleContinuePress = async () => {
    /* Send the cart to the server */
    const URL = `${API_URL}/create-payment-intent`;
    const response = await fetch(URL, {
      method: "POST",
      headers: {
        "Content-Type": "application-json",
      },
      body: JSON.stringify(cart),
    });

    /* Await the response */
    const { publishableKey, clientSecret, merchantName } =
      await response.json();

    /* Navigate to the CheckoutScreen */
    /* You can use navigation.navigate from react-navigation */
    navigateToCheckout({
      publishableKey,
      clientSecret,
      merchantName,
      cart,
      products,
    });
  };

  return (
    <View style={styles.screen}>
      {products.map((p) => {
        return (
          <ProductRow key={p.id} product={p} cart={cart} setCart={setCart} />
        );
      })}
      <View style={{ marginTop: 16 }}>
        <Button title="Continue" onPress={handleContinuePress} />
      </View>
    </View>
  );
};

/**
 * CheckoutScreen related components
 */

const CartInfo = ({ products, cart }) => {
  return (
    <View>
      {Object.keys(cart).map((productId) => {
        const product = products.filter((p) => p.id === productId)[0];
        const quantity = cart[productId];
        return (
          <View
            key={productId}
            style={[{ flexDirection: "row" }, styles.productRow]}
          >
            <Text style={{ flexGrow: 1, fontSize: 17 }}>
              {quantity} x {product.name}
            </Text>
            <Text style={{ fontWeight: "700", fontSize: 17 }}>
              {quantity * product.price}$
            </Text>
          </View>
        );
      })}
    </View>
  );
};

const MethodSelector = ({ onPress, paymentMethod }) => {
  // ...
  return (
    <View style={{ marginVertical: 48, width: "75%" }}>
      <Text
        style={{
          fontSize: 14,
          letterSpacing: 1.5,
          color: "black",
          textTransform: "uppercase",
        }}
      >
        Select payment method
      </Text>
      {/* If there's no paymentMethod selected, show the options */}
      {!paymentMethod && (
        <Pressable
          onPress={onPress}
          style={{
            flexDirection: "row",
            paddingVertical: 8,
            alignItems: "center",
          }}
        >
          {Platform.select({
            ios: <ApplePayMark height={59} />,
            android: <GooglePayMark height={59} />,
          })}

          <View style={[styles.selectButton, { marginLeft: 16 }]}>
            <Text style={[styles.boldText, { color: "#007DFF" }]}>Card</Text>
          </View>
        </Pressable>
      )}
      {/* If there's a paymentMethod selected, show it */}
      {!!paymentMethod && (
        <Pressable
          onPress={onPress}
          style={{
            flexDirection: "row",
            justifyContent: "space-between",
            alignItems: "center",
            paddingVertical: 8,
          }}
        >
          {paymentMethod.label.toLowerCase().includes("apple") && (
            <ApplePayMark height={59} />
          )}
          {paymentMethod.label.toLowerCase().includes("google") && (
            <GooglePayMark height={59} />
          )}
          {!paymentMethod.label.toLowerCase().includes("google") &&
            !paymentMethod.label.toLowerCase().includes("apple") && (
              <View style={[styles.selectButton, { marginRight: 16 }]}>
                <Text style={[styles.boldText, { color: "#007DFF" }]}>
                  {paymentMethod.label}
                </Text>
              </View>
            )}
          <Text style={[styles.boldText, { color: "#007DFF", flex: 1 }]}>
            Change payment method
          </Text>
        </Pressable>
      )}
    </View>
  );
};

const CheckoutScreen = ({
  products,
  navigateBack,
  publishableKey,
  clientSecret,
  merchantName,
  cart,
}) => {
  // We will store the selected paymentMethod
  const [paymentMethod, setPaymentMethod] = React.useState();

  // Import some stripe functions
  const { initPaymentSheet, presentPaymentSheet, confirmPaymentSheetPayment } =
    useStripe();

  // Initialize stripe values upon mounting the screen
  React.useEffect(() => {
    (async () => {
      await initStripe({
        publishableKey,
        // Only if implementing applePay
        // Set the merchantIdentifier to the same
        // value in the StripeProvider and
        // striple plugin in app.json
        merchantIdentifier: "yourMerchantIdentifier",
      });

      // Initialize the PaymentSheet with the paymentIntent data,
      // we will later present and confirm this
      await initializePaymentSheet();
    })();
  }, []);

  const initializePaymentSheet = async () => {
    const { error, paymentOption } = await initPaymentSheet({
      paymentIntentClientSecret: clientSecret,
      customFlow: true,
      merchantDisplayName: merchantName,
      style: "alwaysDark", // If darkMode
      googlePay: true, // If implementing googlePay
      applePay: true, // If implementing applePay
      merchantCountryCode: "ES", // Countrycode of the merchant
      testEnv: __DEV__, // Set this flag if it's a test environment
    });
    if (error) {
      console.log(error);
    } else {
      // Upon initializing if there's a paymentOption
      // of choice it will be filled by default
      setPaymentMethod(paymentOption);
    }
  };

  const handleSelectMethod = async () => {
    const { error, paymentOption } = await presentPaymentSheet({
      confirmPayment: false,
    });
    if (error) {
      alert(`Error code: ${error.code}`, error.message);
    }
    setPaymentMethod(paymentOption);
  };

  const handleBuyPress = async () => {
    if (paymentMethod) {
      const response = await confirmPaymentSheetPayment();

      if (response.error) {
        alert(`Error ${response.error.code}`);
        console.error(response.error.message);
      } else {
        alert("Purchase completed!");
      }
    }
  };

  return (
    <View style={styles.screen}>
      <CartInfo cart={cart} products={products} />
      <MethodSelector
        onPress={handleSelectMethod}
        paymentMethod={paymentMethod}
      />
      <View
        style={{
          flexDirection: "row",
          justifyContent: "space-between",
          alignSelf: "stretch",
          marginHorizontal: 24,
        }}
      >
        <Pressable onPress={navigateBack}>
          <Text style={[styles.textButton, styles.boldText]}>Back</Text>
        </Pressable>
        <Pressable style={styles.buyButton} onPress={handleBuyPress}>
          <Text style={[styles.boldText, { color: "white" }]}>Buy</Text>
        </Pressable>
      </View>
    </View>
  );
};

const AppContent = () => {
  const products = [
    {
      price: 10,
      name: "Pizza Pepperoni",
      id: "pizza-pepperoni",
    },
    {
      price: 12,
      name: "Pizza 4 Fromaggi",
      id: "pizza-fromaggi",
    },
    {
      price: 8,
      name: "Pizza BBQ",
      id: "pizza-bbq",
    },
  ];

  const [screenProps, setScreenProps] = React.useState(null);

  const navigateToCheckout = (screenProps) => {
    setScreenProps(screenProps);
  };

  const navigateBack = () => {
    setScreenProps(null);
  };

  return (
    <View style={styles.container}>
      {!screenProps && (
        <ProductsScreen
          products={products}
          navigateToCheckout={navigateToCheckout}
        />
      )}
      {!!screenProps && (
        <CheckoutScreen {...screenProps} navigateBack={navigateBack} />
      )}
    </View>
  );
};

export default function App() {
  return (
    <StripeProvider>
      <AppContent />
    </StripeProvider>
  );
}

所以有了这段代码,我就能够获得应用程序 运行 并且商品已添加到购物车,但是当我点击卡片选项时,出现错误。

我认为错误是在 CheckoutScreen 上产生的。

error showing

看起来是这样的:

  1. 您的代码调用了 presentPaymentSheet
  2. 它在内部调用了 flowController?.presentPaymentOptions
  3. 它检查并发现 FlowController 没有正确初始化并且 发出 错误.

FlowController 未正确初始化的原因是因为传入了 null or empty 客户端密码。您需要检查 clientSecret 变量是否来自 navigateToCheckout 实际上有一个值。