前提条件
在将 Dodo Payments 集成到您的移动应用之前,请确保您拥有:- Dodo Payments 账户:具有 API 访问权限的活跃商户账户
- API 凭证:来自仪表板的 API 密钥和 webhook 密钥
- 移动应用项目:Android、iOS、React Native 或 Flutter 应用程序
- 后端服务器:安全处理结账会话创建
集成工作流程
移动集成遵循一个安全的 4 步骤流程,其中您的后端处理 API 调用,而您的移动应用管理用户体验。1
后端:创建结账会话
结账会话 API 文档
了解如何在您的后端使用 Node.js、Python 等创建结账会话。请参阅专门的 结账会话 API 文档中的完整示例和参数参考。
安全性:结账会话必须在您的后端服务器上创建,绝不能在移动应用中创建。这可以保护您的 API 密钥并确保正确的验证。
2
移动:获取结账 URL
您的移动应用调用您的后端以获取结账 URL:
- iOS (Swift)
- Android (Kotlin)
- React Native (JavaScript)
复制
func getCheckoutURL(productId: String, customerEmail: String, customerName: String) async throws -> String {
let url = URL(string: "https://your-backend.com/api/create-checkout-session")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestData: [String: Any] = [
"productId": productId,
"customerEmail": customerEmail,
"customerName": customerName
]
request.httpBody = try JSONSerialization.data(withJSONObject: requestData)
let (data, _) = try await URLSession.shared.data(for: request)
let response = try JSONDecoder().decode(CheckoutResponse.self, from: data)
return response.checkout_url
}
复制
suspend fun getCheckoutURL(productId: String, customerEmail: String, customerName: String): String {
val client = OkHttpClient()
val requestBody = JSONObject().apply {
put("productId", productId)
put("customerEmail", customerEmail)
put("customerName", customerName)
}.toString().toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://your-backend.com/api/create-checkout-session")
.post(requestBody)
.build()
val response = client.newCall(request).execute()
val responseBody = response.body?.string()
val jsonResponse = JSONObject(responseBody ?: "")
return jsonResponse.getString("checkout_url")
}
复制
const getCheckoutURL = async (productId, customerEmail, customerName) => {
try {
const response = await fetch('https://your-backend.com/api/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productId,
customerEmail,
customerName
})
});
const data = await response.json();
return data.checkout_url;
} catch (error) {
console.error('Failed to get checkout URL:', error);
throw error;
}
};
安全性:移动应用仅与您的后端通信,绝不直接与 Dodo Payments API 通信。
3
移动:在浏览器中打开结账
在安全的应用内浏览器中打开结账 URL 以进行支付处理。
查看平台特定的集成示例
查看 Android、iOS 和 Flutter 移动支付的完整代码和设置说明。
4
后端:处理支付完成
通过 webhook 和重定向 URL 处理支付完成,以确认支付状态。
平台特定集成
在下面选择您的移动平台以获取完整的实现示例:- Android
- iOS
- React Native
- Flutter
Android 集成
Chrome 自定义标签实现
复制
// Add Chrome Custom Tabs dependency to build.gradle
implementation 'androidx.browser:browser:1.5.0'
// In your Activity
class PaymentActivity : AppCompatActivity() {
private var customTabsSession: CustomTabsSession? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize Custom Tabs
customTabsServiceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
customTabsClient = client
customTabsClient?.warmup(0L)
customTabsSession = customTabsClient?.newSession(object : CustomTabsCallback() {
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
// Handle navigation events
}
})
}
override fun onServiceDisconnected(name: ComponentName) {
customTabsClient = null
}
}
CustomTabsClient.bindCustomTabsService(
this,
"com.android.chrome",
customTabsServiceConnection!!
)
// Get checkout URL from backend and launch
lifecycleScope.launch {
try {
val checkoutURL = getCheckoutURL("prod_123", "[email protected]", "Customer Name")
val customTabsIntent = CustomTabsIntent.Builder(customTabsSession)
.build()
customTabsIntent.launchUrl(this@PaymentActivity, Uri.parse(checkoutURL))
} catch (e: Exception) {
// Handle error
Log.e("PaymentActivity", "Failed to get checkout URL", e)
}
}
}
}
iOS 集成
SFSafariViewController 实现
复制
import SafariServices
class PaymentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
let checkoutURL = try await getCheckoutURL(
productId: "prod_123",
customerEmail: "[email protected]",
customerName: "Customer Name"
)
if let url = URL(string: checkoutURL) {
let safariVC = SFSafariViewController(url: url)
present(safariVC, animated: true, completion: nil)
}
} catch {
// Handle error
print("Failed to get checkout URL: \(error)")
}
}
}
}
深度链接处理
配置 URL 方案以处理支付完成重定向:1. 在 Info.plist 中设置 URL 方案:复制
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
复制
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if url.scheme == "myapp" && url.host == "payment-status" {
NotificationCenter.default.post(name: .paymentCompleted, object: url)
return true
}
return false
}
复制
NotificationCenter.default.addObserver(self, selector: #selector(handlePaymentCompletion(_:)), name: .paymentCompleted, object: nil)
@objc func handlePaymentCompletion(_ notification: Notification) {
if let url = notification.object as? URL {
safariVC?.dismiss(animated: true)
// Parse payment status from URL parameters
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let paymentId = components?.queryItems?.first(where: { $0.name == "payment_id" })?.value
let status = components?.queryItems?.first(where: { $0.name == "status" })?.value
// Handle payment completion
}
}
React Native 集成
使用结账链接 (InAppBrowser)
在应用内浏览器中打开 Dodo 的托管结账页面。InAppBrowser 实现
复制
import React from 'react';
import { View, TouchableOpacity, Text, Alert } from 'react-native';
import { InAppBrowser } from 'react-native-inappbrowser-reborn';
const PaymentButton = ({ productId, onPaymentComplete }) => {
const openPaymentLink = async () => {
try {
// Get checkout URL from backend
const checkoutURL = await getCheckoutURL(
productId,
'[email protected]',
'Customer Name'
);
const result = await InAppBrowser.open(checkoutURL, {
// iOS options
dismissButtonStyle: 'cancel',
preferredBarTintColor: '#453AA4',
preferredControlTintColor: 'white',
readerMode: false,
animated: true,
modalPresentationStyle: 'fullScreen',
modalTransitionStyle: 'coverVertical',
modalEnabled: true,
enableBarCollapsing: false,
// Android options
showTitle: true,
toolbarColor: '#453AA4',
secondaryToolbarColor: 'black',
navigationBarColor: 'black',
navigationBarDividerColor: 'white',
enableUrlBarHiding: true,
enableDefaultShare: true,
forceCloseOnRedirection: false,
animations: {
startEnter: 'slide_in_right',
startExit: 'slide_out_left',
endEnter: 'slide_in_left',
endExit: 'slide_out_right'
},
headers: {
'my-custom-header': 'my custom header value'
}
});
// Handle payment completion based on result
if (result.type === 'dismiss') {
// User dismissed the browser
console.log('Payment browser dismissed');
}
} catch (error) {
Alert.alert('Error', 'Failed to open payment page');
console.error('InAppBrowser error:', error);
}
};
return (
<View style={{ padding: 20 }}>
<TouchableOpacity
style={{
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 8,
alignItems: 'center'
}}
onPress={openPaymentLink}
>
<Text style={{ color: 'white', fontSize: 16, fontWeight: 'bold' }}>
Pay Now
</Text>
</TouchableOpacity>
</View>
);
};
export default PaymentButton;
支付回调的深度链接处理
复制
// App.js or your main component
import React, { useEffect } from 'react';
import { Linking } from 'react-native';
const App = () => {
useEffect(() => {
// Handle deep links when app is already running
const handleDeepLink = (url) => {
if (url.includes('payment-status')) {
const urlParams = new URLSearchParams(url.split('?')[1]);
const paymentId = urlParams.get('payment_id');
const status = urlParams.get('status');
// Handle payment completion
handlePaymentCallback(paymentId, status);
}
};
// Listen for deep links
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
// Handle deep link if app was opened from a link
Linking.getInitialURL().then((url) => {
if (url) {
handleDeepLink(url);
}
});
return () => subscription?.remove();
}, []);
const handlePaymentCallback = (paymentId, status) => {
if (status === 'succeeded') {
// Navigate to success screen or show success message
console.log('Payment successful:', paymentId);
} else {
// Handle payment failure
console.log('Payment failed:', paymentId);
}
};
// Your app components...
};
export default App;
包依赖
将此依赖项添加到您的package.json:复制
{
"dependencies": {
"react-native-inappbrowser-reborn": "^6.4.0"
}
}
安装命令
复制
# Install InAppBrowser
npm install react-native-inappbrowser-reborn
cd ios && pod install
Android 配置
添加到android/app/src/main/AndroidManifest.xml:复制
<activity
android:name="com.reactnativeinappbrowserreborn.InAppBrowserActivity"
android:theme="@style/Theme.AppCompat.Dialog"
android:exported="false" />
iOS 配置
添加到ios/YourApp/Info.plist:复制
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
使用 InAppBrowser 以获得原生浏览器体验。处理深度链接以进行支付回调。
备选方案:原生支付 SDK
为了获得更集成的支付体验,具有原生 UI 和完全自定义,Dodo Payments 提供了 React Native SDK。区域合规性:应用内支付 SDK 有区域限制。在 iOS 上,原生支付 SDK 仅在 欧盟(根据 DMA 条款)合法。在 Android 上,它们在用户选择计费 (UCB) 市场中合法。对于其他地区,请使用上述结账链接方法以确保符合 App Store 和 Play Store 的要求。
React Native SDK 集成指南
完整指南,介绍如何将 Dodo 的 React Native SDK 与原生支付表单集成,提供完全的自定义选项和直接支付处理。包括设置、实现和测试说明。
查看 App Store 费用绕过指南 以了解应用内支付集成的区域合规性要求。
Flutter 集成
复制
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<String> getCheckoutURL(String productId, String customerEmail, String customerName) async {
final response = await http.post(
Uri.parse('https://your-backend.com/api/create-checkout-session'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'productId': productId,
'customerEmail': customerEmail,
'customerName': customerName,
}),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['checkout_url'];
} else {
throw Exception('Failed to get checkout URL: ${response.statusCode}');
}
}
class PaymentScreen extends StatefulWidget {
@override
_PaymentScreenState createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
late WebViewController _controller;
String? checkoutURL;
@override
void initState() {
super.initState();
_getCheckoutURL();
}
Future<void> _getCheckoutURL() async {
try {
final url = await getCheckoutURL('prod_123', '[email protected]', 'Customer Name');
setState(() {
checkoutURL = url;
});
} catch (e) {
// Handle error
print('Failed to get checkout URL: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Payment')),
body: checkoutURL == null
? Center(child: CircularProgressIndicator())
: WebView(
initialUrl: checkoutURL,
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller = webViewController;
},
navigationDelegate: (NavigationRequest request) {
if (request.url.contains('payment_id') && request.url.contains('status')) {
// Handle payment completion
final uri = Uri.parse(request.url);
final paymentId = uri.queryParameters['payment_id'];
final status = uri.queryParameters['status'];
handlePaymentCompletion(paymentId, status);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
);
}
void handlePaymentCompletion(String? paymentId, String? status) {
if (status == 'succeeded') {
// Payment successful
} else {
// Payment failed
}
}
}
使用 WebView 处理支付链接。处理重定向 URL 以检测支付完成。
最佳实践
- 安全性:绝不要在应用代码中存储 API 密钥。使用安全存储和 SSL 钉扎。
- 用户体验:显示加载指示器,优雅地处理错误,并提供清晰的消息。
- 测试:使用测试卡,模拟网络错误,并在各种设备上进行测试。
故障排除
常见问题
- WebView 未打开支付链接:确保支付链接有效并使用 HTTPS。
- 未收到回调:检查您的返回 URL 和 webhook 配置。
- API 密钥错误:验证您的 API 密钥是否正确并具有必要的权限。
其他资源
如有问题或需要支持,请联系 [email protected]。