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
相关产品推荐
相关产品推荐

