一、概述
苹果App内购买(In-App Purchase,IAP)是iOS平台上实现会员订阅、虚拟商品购买等功能的标准方式。在uniapp项目中集成IAP需要结合原生插件和JavaScript代码来实现。

二、前置条件
1. 开发者账号要求
-
有效的Apple Developer账号
-
已创建App ID并配置了In-App Purchase功能
-
在App Store Connect中配置了商品信息
2. 技术环境
-
HBuilderX 3.0+
-
iOS 11.0+
-
Xcode 12.0+
-
真机测试设备(模拟器不支持IAP)

三、项目配置
1. manifest.json配置
{
"app-plus": {
"distribute": {
"ios": {
"capabilities": {
"entitlements": {
"com.apple.developer.in-app-payments": ["merchant.com.yourcompany.yourapp"]
}
}
}
}
}
}

2. 原生插件配置
在项目根目录创建 nativeplugins 文件夹,添加苹果支付插件:
// nativeplugins/apple-pay/package.json
{
"name": "apple-pay",
"id": "apple-pay",
"version": "1.0.0",
"description": "苹果App内购买插件",
"_dp_type": "nativeplugin",
"_dp_nativeplugin": {
"ios": {
"plugins": [
{
"type": "module",
"name": "ApplePay",
"class": "ApplePayModule"
}
],
"frameworks": [
"StoreKit.framework"
],
"deploymentTarget": "11.0"
}
}
}
四、代码实现
1. 创建支付管理类
// utils/ApplePayManager.js
class ApplePayManager {
constructor() {
this.isSupported = false;
this.products = [];
this.init();
}
// 初始化
init() {
// 检查是否支持IAP
if (uni.getSystemInfoSync().platform === 'ios') {
this.isSupported = true;
}
}
// 获取商品信息
async getProducts(productIds) {
returnnewPromise((resolve, reject) => {
if (!this.isSupported) {
reject(newError('当前平台不支持苹果支付'));
return;
}
// 调用原生插件获取商品信息
const applePay = uni.requireNativePlugin('apple-pay');
applePay.getProducts({
productIds: productIds
}, (result) => {
if (result.code === 0) {
this.products = result.data;
resolve(result.data);
} else {
reject(newError(result.message));
}
});
});
}
// 购买商品
async purchaseProduct(productId, quantity = 1) {
returnnewPromise((resolve, reject) => {
if (!this.isSupported) {
reject(newError('当前平台不支持苹果支付'));
return;
}
const applePay = uni.requireNativePlugin('apple-pay');
applePay.purchaseProduct({
productId: productId,
quantity: quantity
}, (result) => {
if (result.code === 0) {
resolve(result.data);
} else {
reject(newError(result.message));
}
});
});
}
// 恢复购买
async restorePurchases() {
returnnewPromise((resolve, reject) => {
if (!this.isSupported) {
reject(newError('当前平台不支持苹果支付'));
return;
}
const applePay = uni.requireNativePlugin('apple-pay');
applePay.restorePurchases((result) => {
if (result.code === 0) {
resolve(result.data);
} else {
reject(newError(result.message));
}
});
});
}
// 验证收据
async verifyReceipt(receiptData) {
returnnewPromise((resolve, reject) => {
// 发送到服务器验证
uni.request({
url: 'https://your-server.com/verify-receipt',
method: 'POST',
data: {
receipt: receiptData
},
success: (res) => {
if (res.data.success) {
resolve(res.data);
} else {
reject(newError(res.data.message));
}
},
fail: (err) => {
reject(err);
}
});
});
}
}
exportdefault ApplePayManager;
2. 页面实现示例
<!-- pages/membership/index.vue -->
<template>
<view class="membership-page">
<view class="header">
<text class="title">会员中心</text>
</view>
<view class="plans-container">
<view
v-for="plan in membershipPlans"
:key="plan.id"
class="plan-item"
:class="{ active: selectedPlan?.id === plan.id }"
@click="selectPlan(plan)"
>
<view class="plan-header">
<text class="plan-name">{{ plan.name }}</text>
<text class="plan-price">¥{{ plan.price }}</text>
</view>
<view class="plan-features">
<text v-for="feature in plan.features" :key="feature" class="feature">
• {{ feature }}
</text>
</view>
</view>
</view>
<view class="purchase-section">
<button
class="purchase-btn"
:disabled="!selectedPlan || purchasing"
@click="handlePurchase"
>
{{ purchasing ? '购买中...' : '立即购买' }}
</button>
<button
class="restore-btn"
:disabled="restoring"
@click="handleRestore"
>
{{ restoring ? '恢复中...' : '恢复购买' }}
</button>
</view>
</view>
</template>
<script>
import ApplePayManager from '@/utils/ApplePayManager.js';
export default {
data() {
return {
applePayManager: null,
membershipPlans: [
{
id: 'monthly_membership',
name: '月度会员',
price: '18.00',
features: ['无广告体验', '专属内容', '优先客服']
},
{
id: 'yearly_membership',
name: '年度会员',
price: '158.00',
features: ['无广告体验', '专属内容', '优先客服', '8折优惠']
}
],
selectedPlan: null,
purchasing: false,
restoring: false
};
},
onLoad() {
this.initApplePay();
},
methods: {
// 初始化苹果支付
async initApplePay() {
try {
this.applePayManager = new ApplePayManager();
// 获取商品信息
const productIds = this.membershipPlans.map(plan => plan.id);
const products = await this.applePayManager.getProducts(productIds);
// 更新商品信息
this.updateProductInfo(products);
} catch (error) {
console.error('初始化苹果支付失败:', error);
uni.showToast({
title: '支付功能初始化失败',
icon: 'none'
});
}
},
// 更新商品信息
updateProductInfo(products) {
products.forEach(product => {
const plan = this.membershipPlans.find(p => p.id === product.productId);
if (plan) {
plan.price = product.localizedPrice;
plan.description = product.localizedDescription;
}
});
},
// 选择会员计划
selectPlan(plan) {
this.selectedPlan = plan;
},
// 处理购买
async handlePurchase() {
if (!this.selectedPlan) {
uni.showToast({
title: '请选择会员计划',
icon: 'none'
});
return;
}
this.purchasing = true;
try {
// 执行购买
const result = await this.applePayManager.purchaseProduct(this.selectedPlan.id);
// 验证收据
await this.applePayManager.verifyReceipt(result.receipt);
// 更新用户会员状态
await this.updateUserMembership(result);
uni.showToast({
title: '购买成功!',
icon: 'success'
});
// 跳转到会员页面
uni.navigateTo({
url: '/pages/membership/success'
});
} catch (error) {
console.error('购买失败:', error);
uni.showToast({
title: error.message || '购买失败,请重试',
icon: 'none'
});
} finally {
this.purchasing = false;
}
},
// 处理恢复购买
async handleRestore() {
this.restoring = true;
try {
const restoredPurchases = await this.applePayManager.restorePurchases();
if (restoredPurchases.length > 0) {
// 更新用户会员状态
await this.updateUserMembership(restoredPurchases[0]);
uni.showToast({
title: '恢复购买成功!',
icon: 'success'
});
} else {
uni.showToast({
title: '没有找到可恢复的购买',
icon: 'none'
});
}
} catch (error) {
console.error('恢复购买失败:', error);
uni.showToast({
title: error.message || '恢复购买失败',
icon: 'none'
});
} finally {
this.restoring = false;
}
},
// 更新用户会员状态
async updateUserMembership(purchaseResult) {
return new Promise((resolve, reject) => {
uni.request({
url: 'https://your-server.com/api/membership/update',
method: 'POST',
data: {
userId: uni.getStorageSync('userId'),
purchaseData: purchaseResult
},
success: (res) => {
if (res.data.success) {
// 更新本地存储
uni.setStorageSync('userMembership', res.data.membership);
resolve(res.data);
} else {
reject(new Error(res.data.message));
}
},
fail: reject
});
});
}
}
};
</script>
<style scoped>
.membership-page {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.plans-container {
margin-bottom: 40rpx;
}
.plan-item {
background: white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
border: 2rpx solid #e0e0e0;
transition: all 0.3s ease;
}
.plan-item.active {
border-color: #007AFF;
background-color: #f0f8ff;
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.plan-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.plan-price {
font-size: 36rpx;
font-weight: bold;
color: #007AFF;
}
.plan-features {
margin-bottom: 20rpx;
}
.feature {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.purchase-section {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.purchase-btn {
background-color: #007AFF;
color: white;
border-radius: 12rpx;
padding: 24rpx;
font-size: 32rpx;
font-weight: bold;
}
.purchase-btn:disabled {
background-color: #ccc;
}
.restore-btn {
background-color: transparent;
color: #007AFF;
border: 2rpx solid #007AFF;
border-radius: 12rpx;
padding: 24rpx;
font-size: 28rpx;
}
.restore-btn:disabled {
color: #ccc;
border-color: #ccc;
}
</style>
3. 原生插件实现(iOS)
// ApplePayModule.h
#import <Foundation/Foundation.h>
#import "DCUniModule.h"
#import <StoreKit/StoreKit.h>
@interface ApplePayModule : DCUniModule <SKProductsRequestDelegate, SKPaymentTransactionObserver>
@end
// ApplePayModule.m
#import "ApplePayModule.h"
@implementation ApplePayModule
- (void)getProducts:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSArray *productIds = options[@"productIds"];
if (!productIds || productIds.count == 0) {
callback(@{@"code": @(-1), @"message": @"商品ID不能为空"}, NO);
return;
}
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIds]];
request.delegate = self;
[request start];
// 保存回调
objc_setAssociatedObject(request, "callback", callback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
UniModuleKeepAliveCallback callback = objc_getAssociatedObject(request, "callback");
NSMutableArray *products = [NSMutableArray array];
for (SKProduct *product in response.products) {
[products addObject:@{
@"productId": product.productIdentifier,
@"localizedTitle": product.localizedTitle,
@"localizedDescription": product.localizedDescription,
@"localizedPrice": [self formatPrice:product.price locale:product.priceLocale]
}];
}
callback(@{@"code": @0, @"data": products}, NO);
}
- (void)purchaseProduct:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSString *productId = options[@"productId"];
NSInteger quantity = [options[@"quantity"] integerValue];
if (!productId) {
callback(@{@"code": @(-1), @"message": @"商品ID不能为空"}, NO);
return;
}
// 检查是否可以支付
if (![SKPaymentQueue canMakePayments]) {
callback(@{@"code": @(-1), @"message": @"设备不支持支付"}, NO);
return;
}
// 创建支付请求
SKMutablePayment *payment = [SKMutablePayment paymentWithProductIdentifier:productId];
payment.quantity = quantity;
// 添加到支付队列
[[SKPaymentQueue defaultQueue] addPayment:payment];
// 保存回调
objc_setAssociatedObject(payment, "callback", callback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
UniModuleKeepAliveCallback callback = objc_getAssociatedObject(transaction.payment, "callback");
switch (transaction.transactionState) {
caseSKPaymentTransactionStatePurchased:
// 购买成功
[self handlePurchasedTransaction:transaction callback:callback];
break;
caseSKPaymentTransactionStateFailed:
// 购买失败
[self handleFailedTransaction:transaction callback:callback];
break;
caseSKPaymentTransactionStateRestored:
// 恢复购买
[self handleRestoredTransaction:transaction callback:callback];
break;
default:
break;
}
}
}
- (void)handlePurchasedTransaction:(SKPaymentTransaction *)transaction callback:(UniModuleKeepAliveCallback)callback {
// 获取收据数据
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
NSString *receiptString = [receiptData base64EncodedStringWithOptions:0];
callback(@{@"code": @0, @"data": @{
@"transactionId": transaction.transactionIdentifier,
@"productId": transaction.payment.productIdentifier,
@"receipt": receiptString
}}, NO);
// 完成交易
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)handleFailedTransaction:(SKPaymentTransaction *)transaction callback:(UniModuleKeepAliveCallback)callback {
NSString *errorMessage = transaction.error.localizedDescription;
callback(@{@"code": @(-1), @"message": errorMessage}, NO);
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)handleRestoredTransaction:(SKPaymentTransaction *)transaction callback:(UniModuleKeepAliveCallback)callback {
// 处理恢复的购买
callback(@{@"code": @0, @"data": @{
@"transactionId": transaction.transactionIdentifier,
@"productId": transaction.payment.productIdentifier
}}, NO);
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)restorePurchases:(UniModuleKeepAliveCallback)callback {
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
// 保存回调用于处理恢复结果
objc_setAssociatedObject(self, "restoreCallback", callback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)formatPrice:(NSDecimalNumber *)price locale:(NSLocale *)locale {
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterCurrencyStyle;
formatter.locale = locale;
return [formatter stringFromNumber:price];
}
@end
五、测试与调试
1. 沙盒测试
-
在App Store Connect中创建沙盒测试用户
-
在设备上使用沙盒账号登录
-
测试购买流程
2. 调试技巧
// 添加调试日志
console.log('商品信息:', products);
console.log('购买结果:', result);
// 检查收据
console.log('收据数据:', receiptData);
3. 常见错误处理
// 错误处理示例
try {
const result = await applePayManager.purchaseProduct(productId);
} catch (error) {
if (error.code === 'E_ALREADY_OWNED') {
// 用户已拥有该商品
uni.showModal({
title: '提示',
content: '您已购买过此商品,是否恢复购买?',
success: (res) => {
if (res.confirm) {
this.handleRestore();
}
}
});
} elseif (error.code === 'E_CANCELLED') {
// 用户取消购买
console.log('用户取消购买');
} else {
// 其他错误
uni.showToast({
title: error.message,
icon: 'none'
});
}
}
六、常见问题
1. 商品信息获取失败
-
检查App Store Connect中的商品配置
-
确认Bundle ID匹配
-
检查网络连接
2. 购买失败
-
确认设备支持IAP
-
检查Apple ID登录状态
-
验证沙盒测试账号
3. 收据验证失败
-
检查服务器端验证逻辑
-
确认收据数据格式正确
-
验证Apple服务器连接

七、最佳实践
1. 用户体验
-
提供清晰的商品描述
-
显示加载状态
-
处理各种错误情况
-
提供恢复购买功能
2. 安全性
-
服务器端验证收据
-
存储购买记录
-
防止重复购买
-
保护用户隐私
3. 性能优化
-
缓存商品信息
-
异步处理购买流程
-
优化网络请求
-
减少不必要的API调用
4. 代码组织
-
模块化设计
-
错误处理统一
-
日志记录完整
-
代码注释清晰
八、总结
通过以上步骤,您可以在uniapp项目中成功集成苹果App内购买功能。记住要:
-
正确配置项目权限
-
实现完整的购买流程
-
添加服务器端验证
-
进行充分的测试
-
遵循苹果的审核指南
这样就能为用户提供安全、便捷的会员购买体验。
uniapp官方Apple支付文档:
https://uniapp.dcloud.net.cn/tutorial/app-payment-aip.html#%E8%8E%B7%E5%8F%96%E5%BA%94%E7%94%A8%E5%86%85%E6%94%AF%E4%BB%98%E5%AF%B9%E8%B1%A1
IT技术交流
3158

被折叠的 条评论
为什么被折叠?



