我有一个用 Flutter 编写的应用程序,我使用 Stripe SDK 11.0.0 在应用程序中实现了一次性支付(例如游戏点)。我的后端和应用程序非常适合一次性付款场景。我在手机屏幕上看到成功,并在 Stripe 仪表板上看到付款
现在我想实现订阅付费。
您可以看到到目前为止我在整个视图模型类中仅针对该场景所做的工作:
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:get/get.dart';
import 'package:my_app/data/response/Status.dart';
import 'package:my_app/repository/marketing/MarketingRepository.dart';
import 'package:my_app/view_models/user_preference/UserPreference.dart';
class SubscriptionViewModel extends GetxController {
final _api = MarketingRepository();
UserPreference userPreference = UserPreference();
final rxRequestStatus = Status.LOADING.obs;
RxString clientSecret = ''.obs;
RxString ephemeralKey = ''.obs;
RxString customerId = ''.obs;
/// Handles subscription for all three cases
Future<void> handleSubscription(String plan) async {
try {
var user = await userPreference.getUser();
// Step 1: Make request to the backend to check for payment method and retrieve necessary information
Map<String, dynamic> data = {
MY_REQUEST_BODY_HERE
};
// API call to backend to initiate subscription
var response = await _api.paymentApi(
data,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer ${user.accessToken}"
},
);
// Handle the response based on the status
if (response["status"] == "requires_payment_method") {
// Case 1: No payment method available, collect payment details
print('No payment method available, need to collect payment details');
await collectPaymentMethod(
response["setupIntentClientSecret"],
response["customerId"],
response["ephemeralKey"],
plan
);
} else if (response["status"] == "success" && response["payload"]["clientSecret"] != null) {
if (response["payload"]["ephemeralKey"] != null) {
// Case 2: User has a payment method but it's not the default, retrieve and use it
print('User has a payment method, but not default.');
await confirmSubscription(response["payload"]["clientSecret"], response["payload"]["ephemeralKey"], response["payload"]["customerId"]);
} else {
// Case 3: User has a default payment method, directly pop up payment sheet
print('User has a default payment method. Confirming subscription.');
await confirmSubscription(response["payload"]["clientSecret"], "", response["payload"]["customerId"]);
}
}
} catch (e) {
print('Error during subscription: $e');
if (e is StripeException) {
print('Stripe error: ${e.error.localizedMessage}');
} else {
print('Unexpected error: $e');
}
}
}
/// Collect payment method when none is available (Case 1)
Future<void> collectPaymentMethod(String setupIntentClientSecret, String customerId, String ephemeralKey, String plan) async {
try {
// Initialize payment sheet for collecting new payment method
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
customFlow: false,
merchantDisplayName: 'My Store',
setupIntentClientSecret: setupIntentClientSecret,
customerEphemeralKeySecret: ephemeralKey,
customerId: customerId,
),
);
// Present the payment sheet for the user to enter payment details
await Stripe.instance.presentPaymentSheet();
print('Payment method successfully added!');
// Retrieve the payment method from the SetupIntent after the payment sheet is completed
final paymentMethodId = await retrievePaymentMethodIdFromSetupIntent(setupIntentClientSecret);
if (paymentMethodId != null) {
print('PaymentMethodId: $paymentMethodId');
// Send the paymentMethodId to the backend to set it as default and complete the subscription
var user2 = await userPreference.getUser();
// Step 1: Make request to the backend to check for payment method and retrieve necessary information
Map<String, dynamic> data2 = {
MY_REQUEST_BODY_HERE
'paymentMethodId': paymentMethodId,
};
// API call to backend to initiate subscription
await _api.paymentApi(
data2,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer ${user2.accessToken}"
},
);
// Show successful subscription
Get.snackbar('Success', 'Subscription completed successfully!');
}
} catch (e) {
print('Error collecting payment method: $e');
if (e is StripeException) {
print('Stripe error: ${e.error.localizedMessage}');
} else {
print('Unexpected error: $e');
}
}
}
/// Helper method to retrieve the paymentMethodId from a SetupIntent
Future<String?> retrievePaymentMethodIdFromSetupIntent(String setupIntentClientSecret) async {
try {
// Fetch the SetupIntent to retrieve the attached payment method
final setupIntent = await Stripe.instance.retrieveSetupIntent(setupIntentClientSecret);
if (setupIntent.paymentMethodId != null) {
return setupIntent.paymentMethodId; // Retrieve the payment method ID
} else {
print('No payment method was found in the SetupIntent.');
return null;
}
} catch (e) {
print('Error retrieving payment method from SetupIntent: $e');
return null;
}
}
/// Confirm subscription for users with a payment method (Case 2 & Case 3)
Future<void> confirmSubscription(String clientSecret, String ephemeralKey, String customerId) async {
try {
// Initialize payment sheet for confirmation
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
customFlow: false,
merchantDisplayName: 'My Store',
paymentIntentClientSecret: clientSecret,
customerEphemeralKeySecret: ephemeralKey, // This will be empty for default payment methods
customerId: customerId,
),
);
// Present the payment sheet to confirm the subscription
await Stripe.instance.presentPaymentSheet().then((e) {
Stripe.instance.confirmPaymentSheetPayment();
});
print('Subscription confirmed successfully!');
// Show successful subscription
Get.snackbar('Success', 'Subscription completed successfully!');
} catch (e) {
print('Error confirming subscription: $e');
if (e is StripeException) {
print('Stripe error: ${e.error.localizedMessage}');
} else {
print('Unexpected error: $e');
}
}
}
}
当我按下按钮时,它会按下
handleSubscription
方法。简而言之,它向我的后端发送请求,其中包含一些参数。
如果我在第一次时间点击,我的后端会像这样返回我;
{
"customerId": "cus_Qy9...",
"ephemeralKey": "ek_test_YWN...",
"message": "Customer has no default payment method. Please provide a payment method.",
"setupIntentClientSecret": "seti_1Q6...",
"status": "requires_payment_method"
}
在我的代码中,它连接到 Case1(这意味着
collectPaymentMethod
)。我看到我的屏幕是这样的:
我想我做对了,因为如果我输入测试卡号并点击
Set Up
按钮,我会看到按钮呈绿色并带有绿色勾号,然后我会在 Stripe 仪表板上看到我的订阅。
然后,如果我从 Stripe Dashboard 取消订阅,并且如果我第二次尝试从我的应用程序中进行另一个订阅,我的后端会向我返回如下响应:
{
"payload": {
"clientSecret": "pi_3Q6...",
"customerId": "cus_Qy9...",
"ephemeralKey": "ek_test_YWN...",
"id": "sub_1Q6...",
"status": "active"
},
"status": "success",
"transaction_type": "subscription"
}
但是在我的屏幕上,我看到一个加载微调器,然后它就消失了。我看到的是这样的:
在我的日志上,我看到了那些;
I/flutter (21575): User has a payment method, but not default.
I/ContentCaptureHelper(21575): Setting logging level to OFF
I/flutter (21575): Error confirming subscription: StripeException(error: LocalizedErrorMessage(code: FailureCode.Failed, localizedMessage: PaymentSheet cannot set up a PaymentIntent in status 'succeeded'.
I/flutter (21575): See https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status., message: PaymentSheet cannot set up a PaymentIntent in status 'succeeded'.
I/flutter (21575): See https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status., stripeErrorCode: null, declineCode: null, type: null))
I/flutter (21575): Stripe error: PaymentSheet cannot set up a PaymentIntent in status 'succeeded'.
I/flutter (21575): See https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status.
W/WindowOnBackDispatcher(21575): sendCancelIfRunning: isInProgress=falsecallback=androidx.activity.OnBackPressedDispatcher$Api34Impl$createOnBackAnimationCallback$1@a9a0ae4
奇怪的是;我在 Stripe Dashboard 上看到我的新订阅。
第一个问题; 在案例 1 中,我看到按钮为“设置”。这是正确的并且一切都正确吗?
第二个问题; 由于我看到错误日志&只是一个微调器,并在我的手机上消失,但在 Stripe Dashboard 上成功订阅,您能告诉我我错过了什么吗?我做错了什么?
如果您认为我的移动扑动端的一切都是正确的,我可以编辑问题并在此处添加我的后端代码。
我没有查看您的整个代码片段,因为您确实应该只共享遇到问题的部分。您也没有共享创建订阅对象的服务器端代码。
根据您共享的内容,您似乎正在为现有的 Customer 对象创建订阅,并且可能使用
collection_method: charge_automatically
。创建订阅后,Stripe 会尝试在第一张发票上收取付款,如果客户没有 invoice_settings.default_payment_method
的值,则会失败。这将带您进入案例 1,我认为您尝试使用订阅的 pending_setup_intent
来收集付款详细信息。
我假设您创建另一个订阅的步骤是针对同一客户(“cus_Qy9...”),并且之前保存的卡已保存为其默认付款方式。您看到错误和加载旋转器消失的原因可能是因为您的代码尝试使用已确认的 PaymentIntent 的客户端密钥来呈现 PaymentSheet。如果您发现订阅的最新发票已支付,则无需出示付款单;你应该引导你的客户“成功!”页面或显示有关其新创建的订阅的详细信息的页面。