UniApp集成Apple Pay支付完整指南

该文章已生成可运行项目,

一、概述

苹果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内购买功能。记住要:

  1. 正确配置项目权限

  2. 实现完整的购买流程

  3. 添加服务器端验证

  4. 进行充分的测试

  5. 遵循苹果的审核指南

这样就能为用户提供安全、便捷的会员购买体验。

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技术交流

本文章已经生成可运行项目
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前后端AI实战开发

你的钟意将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值