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

基于flutter_bloc的Stripe支付集成出现异步挂起错误

Flutter Stripe支付集成问题:Bloc异步处理中出现JSON解析格式错误

几周前Flutter Stripe支付功能可正常完成,现在使用flutter_bloc处理Event时出现异步挂起错误,已严格遵循Bloc官方文档使用async/await,但问题仍未解决。

错误信息

[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: FormatException: Unexpected character (at line 2, character 1)
<html><head>
^
#0      _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1383:5)
#1      _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1250:9)
#2      _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:915:22)
#3      _parseJson (dart:convert-patch/convert_patch.dart:35:10)
#4      JsonDecoder.convert (dart:convert/json.dart:612:36)
#5      JsonCodec.decode (dart:convert/json.dart:216:41)
#6      PaymentBloc._callPayEndpointMethodId
package:flutter_app_social/…/paymentbloc/payment_bloc.dart:136
<asynchronous suspension>
#7      PaymentBloc._onPaymentCreateIntent
package:flutter_app_social/…/paymentbloc/payment_bloc.dart:38
<asynchronous suspension>
#8      Bloc.on.<anonymous closure>.handleEvent
package:bloc/src/bloc.dart:226
<asynchronous suspension>

源码文件

payment_state.dart

part of 'payment_bloc.dart';

enum PaymentStatus { initial, loading, success, failure }

class PaymentState extends Equatable {
  final PaymentStatus status;
  final CardFieldInputDetails inputDetails;

  const PaymentState({
    this.status = PaymentStatus.initial,
    this.inputDetails = const CardFieldInputDetails(complete: false),
  });

  PaymentState copyWith({
    PaymentStatus? status,
    CardFieldInputDetails? inputDetails,
  }) {
    return PaymentState(
      status: status ?? this.status,
      inputDetails: inputDetails ?? this.inputDetails,
    );
  }

  @override
  List<Object> get props => [status, inputDetails];
}

payment_event.dart

part of 'payment_bloc.dart';

abstract class PaymentEvent extends Equatable {
  const PaymentEvent();

  @override
  List<Object> get props => [];
}

class PaymentStart extends PaymentEvent {}

class PaymentCreateIntent extends PaymentEvent {
  final BillingDetails billingDetails;
  final List<Map<String, dynamic>> items;

  const PaymentCreateIntent({
    required this.billingDetails,
    required this.items,
  });

  @override
  List<Object> get props => [billingDetails, items];
}

class PaymentConfirmIntent extends PaymentEvent {
  final String clientSecret;

  const PaymentConfirmIntent({
    required this.clientSecret,
  });

  @override
  List<Object> get props => [clientSecret];
}

payment_bloc.dart

import 'dart:async';
import 'dart:convert';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:http/http.dart' as http;

part 'payment_event.dart';
part 'payment_state.dart';

class PaymentBloc extends Bloc<PaymentEvent, PaymentState> {
  PaymentBloc() : super(const PaymentState()) {
    on<PaymentStart>(_onPaymentStart);
    on<PaymentCreateIntent>(_onPaymentCreateIntent);
    on<PaymentConfirmIntent>(_onPaymentConfirmIntent);
  }

  void _onPaymentStart(
    PaymentStart event,
    Emitter<PaymentState> emit,
  ) async {
    emit(state.copyWith(status: PaymentStatus.initial));
  }

  void _onPaymentCreateIntent(
    PaymentCreateIntent event,
    Emitter<PaymentState> emit,
  ) async {
    emit(state.copyWith(status: PaymentStatus.loading));

    final paymentMethod = await Stripe.instance.createPaymentMethod(
      params: PaymentMethodParams.card(
        paymentMethodData:
            PaymentMethodData(billingDetails: event.billingDetails),
      ),
    );

    final paymentIntentResults = await _callPayEndpointMethodId(
      useStripeSdk: true,
      paymentMethodId: paymentMethod.id,
      currency: 'usd',
      items: event.items,
    );

    print(paymentIntentResults);

    if (paymentIntentResults['error'] != null) {
      emit(
        state.copyWith(status: PaymentStatus.failure),
      );
    }

    if (paymentIntentResults['clientSecret'] != null &&
        paymentIntentResults['requiresAction'] == null) {
      emit(
        state.copyWith(status: PaymentStatus.success),
      );
    }

    if (paymentIntentResults['clientSecret'] != null &&
        paymentIntentResults['requiresAction'] == null) {
      final String clientSecret = paymentIntentResults['clientSecret'];
      add(
        PaymentConfirmIntent(clientSecret: clientSecret),
      );
    }
  }

  void _onPaymentConfirmIntent(
    PaymentConfirmIntent event,
    Emitter<PaymentState> emit,
  ) async {
    try {
      final paymentIntent =
          await Stripe.instance.handleNextAction(event.clientSecret);

      if (paymentIntent.status == PaymentIntentsStatus.RequiresConfirmation) {
        Map<String, dynamic> results = await _callPayEndpointIntentId(
          paymentIntentId: paymentIntent.id,
        );
        if (results['error'] != null) {
          emit(state.copyWith(status: PaymentStatus.failure));
        } else {
          emit(state.copyWith(status: PaymentStatus.success));
        }
      }
    } catch (e) {
      print(e);
      emit(
        state.copyWith(status: PaymentStatus.failure),
      );
    }
  }

  Future<Map<String, dynamic>> _callPayEndpointIntentId({
    required String paymentIntentId,
  }) async {
    final url = Uri.parse(
      'https://us-central1-evental-acdgnf69.cloudfunctions.net/StripePayEndpointIntentId',
    );

    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: json.encode(
        {
          'paymentIntentId': paymentIntentId,
        },
      ),
    );
    return json.decode(response.body);
  }

  Future<Map<String, dynamic>> _callPayEndpointMethodId({
    required bool useStripeSdk,
    required String paymentMethodId,
    required String currency,
    required List<Map<String, dynamic>> items,
  }) async {
    final url = Uri.parse(
      'https://us-central1-evental-acdgnf69.cloudfunctions.net/StripePayEndpointMethodId',
    );

    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: json.encode(
        {
          'useStripeSdk': useStripeSdk,
          'paymentMethodId': paymentMethodId,
          'currency': currency,
          'items': items,
        },
      ),
    );
    print(response.body);
    return json.decode(response.body);
  }
}

stripe_form_page.dart 按钮触发代码

ElevatedButton(
                      onPressed: () {
                        (controller.details.complete)
                            ? context.read<PaymentBloc>().add(
                                  const PaymentCreateIntent(...

问题分析与解决方案

问题根源

从错误栈可以明确,错误发生在_callPayEndpointMethodId方法的json.decode(response.body)步骤,原因是后端接口返回的不是预期的JSON数据,而是HTML内容(错误提示中的<html><head>)。这说明你的Stripe后端云函数(StripePayEndpointMethodId)出现了异常,可能是部署故障、权限问题、Stripe密钥配置错误或业务逻辑报错,导致返回了错误页面而非正常JSON响应。

解决步骤

  1. 检查后端云函数状态

    • 确认StripePayEndpointMethodId和StripePayEndpointIntentId云函数是否正常部署并可访问
    • 检查云函数中的Stripe API密钥是否有效,是否有足够的权限完成支付操作
    • 查看云函数的日志,定位具体的后端错误原因
  2. 增加HTTP响应合法性校验
    在两个接口调用方法中,先判断响应状态码是否正常,再尝试解析JSON,避免直接解析错误内容:

    Future<Map<String, dynamic>> _callPayEndpointMethodId({
      required bool useStripeSdk,
      required String paymentMethodId,
      required String currency,
      required List<Map<String, dynamic>> items,
    }) async {
      final url = Uri.parse(
        'https://us-central1-evental-acdgnf69.cloudfunctions.net/StripePayEndpointMethodId',
      );
    
      final response = await http.post(
        url,
        headers: {'Content-Type': 'application/json'},
        body: json.encode(
          {
            'useStripeSdk': useStripeSdk,
            'paymentMethodId': paymentMethodId,
            'currency': currency,
            'items': items,
          },
        ),
      );
      print(response.body);
      // 新增状态码校验
      if (response.statusCode >= 400) {
        throw Exception('请求失败,状态码:${response.statusCode},响应内容:${response.body}');
      }
      try {
        return json.decode(response.body);
      } catch (e) {
        throw Exception('JSON解析失败:$e,响应内容:${response.body}');
      }
    }
    

    另一个_callPayEndpointIntentId方法做同样修改。

  3. 完善Bloc事件处理的异常捕获
    在_onPaymentCreateIntent方法外层增加全局try-catch,避免未处理的异常导致应用崩溃:

    void _onPaymentCreateIntent(
      PaymentCreateIntent event,
      Emitter<PaymentState> emit,
    ) async {
      try {
        emit(state.copyWith(status: PaymentStatus.loading));
    
        final paymentMethod = await Stripe.instance.createPaymentMethod(
          params: PaymentMethodParams.card(
            paymentMethodData:
                PaymentMethodData(billingDetails: event.billingDetails),
          ),
        );
    
        final paymentIntentResults = await _callPayEndpointMethodId(
          useStripeSdk: true,
          paymentMethodId: paymentMethod.id,
          currency: 'usd',
          items: event.items,
        );
    
        print(paymentIntentResults);
    
        if (paymentIntentResults['error'] != null) {
          emit(state.copyWith(status: PaymentStatus.failure));
          return;
        }
    
        if (paymentIntentResults['clientSecret'] != null) {
          if (paymentIntentResults['requiresAction'] == null) {
            emit(state.copyWith(status: PaymentStatus.success));
          } else {
            final String clientSecret = paymentIntentResults['clientSecret'];
            add(PaymentConfirmIntent(clientSecret: clientSecret));
          }
        } else {
          emit(state.copyWith(status: PaymentStatus.failure));
        }
      } catch (e) {
        print(e);
        emit(state.copyWith(status: PaymentStatus.failure));
      }
    }
    
  4. 校验事件触发参数
    确认按钮触发PaymentCreateIntent时,传递的billingDetails和items数据格式正确,没有空值或不符合后端要求的结构。

内容的提问来源于stack exchange,提问作者Alex Chalakov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 13:05:55