You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React-Native iOS扫码转账(Stripe)出现Network request failed错误求助

iOS React-Native Stripe转账应用网络错误排查

我正在开发一款iOS平台的React-Native应用,用户扫描含收款方信息的二维码后,通过Stripe实现个人转账。但扫码后终端报错:{"error": {"error":"TypeError:Network request failed", "status":"FETCH_ERROR"}}。预期流程是扫码后弹出加载了上一页面计算好的付款金额的payment sheet。目前我能通过终端向Stripe发送POST请求,请求会显示在Stripe后台且能收到响应,以下是相关代码,请求排查错误原因:

错误截图

错误截图

相关代码

QRCodeScan.js

import { View,StyleSheet, Text, Button, Alert } from 'react-native'
import React, { useState, useEffect } from 'react'
import { BarCodeScanner } from 'expo-barcode-scanner'
import { useNavigation } from '@react-navigation/native'
import { useCreatePaymentIntentMutation } from '../../store/apiSlice'
import { useRoute } from '@react-navigation/native'
import {useStripe} from '@stripe/stripe-react-native'
const QRCodeScan = () => {
  //This screen is used to scan QR codes and then display the Stripe payment functionality

  const[hasPermission, setHasPermission] = useState('')
  const[scanned, setScanned] = useState(false)
  const[iban, setIban] = useState('Scan a QR Code')
  const [createPaymentIntent] = useCreatePaymentIntentMutation()
  const{initPaymentSheet, presentPaymentSheet} =  useStripe()
  const navi = useNavigation()
  const route = useRoute()

  const Tip = route.params.calculatedTip

  //Asks the device for permission to use the camera and sets it to granted if allowed
  const askForCameraPermission = () =>{
    (async () =>{
      const {status} = await BarCodeScanner.requestPermissionsAsync()
      setHasPermission(status === 'granted')
    })()
  }
  //The display box for the permission request
  useEffect(() =>{
    askForCameraPermission()
  }, [])

  //Scans and stores the information scanned from the barcode and then loads the Stripe payment overlay
  const handleBarCodeScanned = ({type, data}) =>{
    setScanned(true)
    setIban(data)
    console.log("Type: " + type)
    console.log("Data: " + data)
    onMakePayment()
  }

  //Sets the total from the calculated Tip
  const onMakePayment = async () => {
    const response = await createPaymentIntent({amount: route.params.Tip })
    
    console.log(response)

    if(response.error){
      Alert.alert('Something went wrong,', response.error)
      return;
    }

    const { paymentIntent } = response.data;

    //Sets the payment details and sets the employees IBAN as the destination of the payment
  const paymentMethod = {
    type: 'sepa_debit',
    sepa_debit: {
      iban: iban, // set the IBAN to the scanned value
    },
    billing_details: {
      name: '***', // set the name for the billing details
    },
  };

  const { error } = await stripe.createPaymentMethod(paymentMethod);

  if (error) {
    Alert.alert('Something went wrong,', error.message);
    return;
  }

    const initResponse = await initPaymentSheet({
      merchantDisplayName: 'Tipper',
      paymentIntentClientSecret: paymentIntent.client_secret,
      customFlow: true,
      paymentMethodId: paymentMethod.id
      
    })

    if(initResponse.error){
      console.log(initResponse.error)
      Alert.alert('Something went wrong,', response.error)
      return;
    }
    
    await presentPaymentSheet()

  };



  
  //If hasnt been granted or denied, device requests user to either grant or deny permission
  if(hasPermission === null) {
    return(
      <View style={styles.container}>
      <Text>Requesting camera permission</Text>
      </View>
    )
  }
  
  //If permission isnt granted, camera isnt used and displays no access to camera
  if(hasPermission === false){
    return(
      <View style={styles.container}>
      <Text style={{margin: 10}}>No access to camera</Text>
      <Button title={'Allow camera'} onPress={() => askForCameraPermission()}></Button>
      </View>
    )
  }
  return(
    <View style={styles.container}>
    <View >
    <BarCodeScanner 
      onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
      style={{height: 400, width: 400}}>
      </BarCodeScanner>
    </View>
    <Text style={styles.maintext}>{iban}</Text>
    {scanned && <Button title={'Scan again'} onPress={() => setScanned(false)} color='tomato'></Button>}
    </View>
    )
  }
  
  const styles = StyleSheet.create({
    root:{
        alignItems: 'center',
        padding: 25
    },
    title:{
      fontSize: 20,
      fontWeight: 'bold',
      margin: 10
    },
    container: {
        backgroundColor: 'white',
        width: '100%',
        borderColor: '#E8E8E8',
        borderWidth: 1,
        borderRadius: 5,
        paddingHorizontal: 10,
        paddingVertical: 10,
        marginVertical: 7
    },
    value:{
        fontSize: 20,
        fontWeight: 'bold',
        margin: 10
    },
    barcodebox:{
      alignItems: 'center',
      justifyContent: 'center',
      height: 300,
      width: 300,
      overflow: 'hidden',
      borderRadius: 30,
      backgroundColor: 'tomato'
    },
    maintext:{
      fontSize: 16,
      margin: 20
    }
})


export default QRCodeScan

server.js

const express = require('express')
const paymentRoutes = require('./PaymentLogic/PaymentRoute')
const bodyParser = require('body-parser')
const app = express()
const PORT = 3000

app.use(bodyParser.json())
app.use('/payments', paymentRoutes)


app.listen(PORT, () =>{
    console.log('API listening on port ', PORT)
})

PaymentRoute.js

const express = require('express')
const router = express.Router()
const stripe = require('stripe')(/* 已移除密钥 */)

//This is used to post the transaction details to Stripe account for Tipper
router.post('/intents', async(req, res) =>{
    try {
        const paymentIntent = await stripe.paymentIntents.create({
        amount: req.body.amount,
        currency: 'eur',
        automatic_payment_methods:{
           enabled: true 
        }
    })

    res.json({paymentIntent: paymentIntent.client_secret})
    } catch (error) {
        res.status(400).json({
            error: error.message
        })
    }
    
})

module.exports = router

apiSlice.js

//This file is used to create the payments that would be sent to Stripe
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react'

const baseUrl = 'http://localhost:3000/';

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl }),
  endpoints: (builder) => ({
    // Payments
    createPaymentIntent: builder.mutation({
      query: (data) => ({
        url: 'payments/intents',
        method: 'POST',
        body: data,
      }),
    }),
  }),
});

export const {
 useCreatePaymentIntentMutation
} = apiSlice;

store/index.js

import { configureStore } from '@reduxjs/toolkit';
import { apiSlice } from './apiSlice';
 
export const store = configureStore({
  reducer: {

    api: apiSlice.reducer,
  },
  // Adding the api middleware enables caching, invalidation, polling,
  // and other useful features of `rtk-query`.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
})

错误原因及修复方案

1. iOS模拟器无法访问localhost后端

iOS模拟器中localhost指向模拟器自身,而非你的本地开发机器,导致网络请求失败。

  • 修复:在apiSlice.js中将baseUrl替换为你的电脑局域网IP地址(比如http://192.168.1.100:3000/),可通过ifconfig(Mac)或ipconfig(Windows)查看。

2. Stripe实例未正确解构

QRCodeScan.js中调用stripe.createPaymentMethod时,stripe变量未定义,是因为解构useStripe()时没获取该实例。

  • 修复:修改解构语句:
const{initPaymentSheet, presentPaymentSheet, stripe} =  useStripe()

3. Stripe金额单位错误

Stripe要求金额以最小货币单位传入(欧元为分),如果route.params.Tip是欧元的整数(比如10代表10欧元),需要乘以100转换。

  • 修复:调整onMakePayment中的金额参数:
const response = await createPaymentIntent({amount: route.params.Tip * 100 })

4. PaymentSheet初始化参数错误

手动创建的paymentMethod.id不能直接传入initPaymentSheet,需要先将支付方式附加到Payment Intent上。

  • 修复:调整支付流程,添加附加支付方式的步骤:
// 创建payment method后
const { error: attachError } = await stripe.attachPaymentMethod(paymentMethod.id, {
  payment_intent: paymentIntent.id,
});

if (attachError) {
  Alert.alert('Something went wrong,', attachError.message);
  return;
}

// 初始化PaymentSheet时移除paymentMethodId参数
const initResponse = await initPaymentSheet({
  merchantDisplayName: 'Tipper',
  paymentIntentClientSecret: paymentIntent.client_secret,
  customFlow: true,
})

5. iOS网络安全限制

iOS默认阻止HTTP请求,需要在项目配置中添加例外。

  • 修复:在ios/[你的项目名]/Info.plist中添加以下配置:
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains</key>
  <dict>
    <key>你的局域网IP</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>

内容的提问来源于stack exchange,提问作者Adam Dalton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 14:25:03