发布日期: 2026-02-12
文章字数: {{articleLength}}
阅读时长: {{parseInt(articleLength / 800)}} 分
阅读次数: {{read_count}}
苹果官方退款要求用swift的beginRefundRequest方法(IOS15),在测试苹果内购退款时需要用到
https://developer.apple.com/documentation/storekit/transaction/beginrefundrequest(for:in:)-65tph
因此用AI弄了个插件代码到mac上编译了个插件:
```swift
//
// DCIapRefundPluginSwiftHelper.swift
// DCIapRefundPlugin
//
// Created by
// Copyright © 2024 DCIapRefundPlugin. All rights reserved.
//
import Foundation
import StoreKit
import UIKit
// @objcMembers 让所有方法都暴露给 Objective-C
@objcMembers
@MainActor
public class DCIapRefundPluginSwiftHelper: NSObject {
// MARK: - 单例
public static let shared = DCIapRefundPluginSwiftHelper()
private override init() {
super.init()
print("[IAP Swift] ✅ DCIapRefundPluginSwiftHelper 初始化成功")
debugPrintMethods()
}
// MARK: - 调试方法
private func debugPrintMethods() {
var methodCount: UInt32 = 0
guard let methodList = class_copyMethodList(object_getClass(self), &methodCount) else { return }
print("[IAP Swift] 暴露给 Objective-C 的方法列表:")
for i in 0..<Int(methodCount) {
let method = methodList[i]
let selector = method_getName(method)
let methodName = NSStringFromSelector(selector)
print(" - \(methodName)")
}
free(methodList)
}
// MARK: - 获取当前 WindowScene(类方法)
@objc public static func getCurrentWindowScene() -> UIWindowScene? {
for scene in UIApplication.shared.connectedScenes {
if scene.activationState == .foregroundActive,
let windowScene = scene as? UIWindowScene {
print("[IAP Swift] ✅ 获取到 WindowScene: \(windowScene)")
return windowScene
}
}
print("[IAP Swift] ❌ 未找到活跃的 WindowScene")
return nil
}
// MARK: - 退款请求方法(实例方法)
// 显式指定 Objective-C 方法名,确保与调用匹配
@objc(beginRefundRequestWithTransactionID:windowScene:completion:)
public func beginRefundRequest(
transactionID: UInt64,
windowScene: UIWindowScene,
completion: @escaping (String?, Error?) -> Void
) {
print("[IAP Swift] 🚀 开始处理退款请求")
print("[IAP Swift] 交易ID: \(transactionID)")
print("[IAP Swift] WindowScene: \(windowScene)")
// 1. 验证交易ID
guard transactionID > 0 else {
print("[IAP Swift] ❌ 无效的交易ID")
completion("error: invalid transaction ID", nil)
return
}
// 2. 验证 iOS 版本
guard #available(iOS 15.0, *) else {
print("[IAP Swift] ❌ iOS 版本过低,需要 iOS 15+")
completion("error: iOS 15+ required", nil)
return
}
// 3. 执行退款请求
Task { @MainActor in
do {
print("[IAP Swift] 📤 正在向 Apple 发起退款请求...")
let status = try await Transaction.beginRefundRequest(
for: transactionID,
in: windowScene
)
switch status {
case .success:
print("[IAP Swift] ✅ 退款请求提交成功")
completion("success", nil)
case .userCancelled:
print("[IAP Swift] 👤 用户取消了退款请求")
completion("userCancelled", nil)
@unknown default:
print("[IAP Swift] ❓ 未知状态: \(status)")
completion("unknown", nil)
}
} catch let error as StoreKitError {
// StoreKit 特定错误
print("[IAP Swift] ❌ StoreKit错误: \(error.localizedDescription)")
switch error {
case .userCancelled:
completion("userCancelled", nil)
case .notAvailableInStorefront:
completion("error: not available in storefront", nil)
case .networkError(let networkError):
completion("error: network error - \(networkError.localizedDescription)", nil)
default:
completion("error: storekit error - \(error.localizedDescription)", nil)
}
} catch {
// 其他错误
print("[IAP Swift] ❌ 未知错误: \(error.localizedDescription)")
completion(nil, error)
}
}
}
// MARK: - 重载方法:支持直接传入字符串交易ID(方便调试)
@objc(beginRefundRequestWithTransactionIdString:windowScene:completion:)
public func beginRefundRequest(
transactionIdString: String,
windowScene: UIWindowScene,
completion: @escaping (String?, Error?) -> Void
) {
let transactionID = UInt64(transactionIdString) ?? 0
beginRefundRequest(transactionID: transactionID,
windowScene: windowScene,
completion: completion)
}
// MARK: - 检查退款可用性
@objc public static func isRefundAvailable() -> Bool {
if #available(iOS 15.0, *) {
return true
}
return false
}
}
```
```object-c
//
// IapRefundModule.m
// DCIapRefundPlugin
//
// Created by
// Copyright © 2024 DCIapRefundPlugin. All rights reserved.
//
#import "IapRefundModule.h"
#import "DCUniDefine.h"
// 导入 Swift 生成的头文件
// 注意:Xcode 会自动生成这个文件,编译后才会存在
#import "DCIapRefundPlugin-Swift.h"
@implementation IapRefundModule
#pragma mark - 模块生命周期
+ (void)load {
NSLog(@"[IAP ObjC] ========================================");
NSLog(@"[IAP ObjC] ✅ IapRefundModule 类已加载到内存");
NSLog(@"[IAP ObjC] ========================================");
}
+ (void)initialize {
[super initialize];
NSLog(@"[IAP ObjC] 📦 IapRefundModule 初始化");
// 检查 Swift 类是否可用
Class swiftClass = NSClassFromString(@"DCIapRefundPluginSwiftHelper");
if (swiftClass) {
NSLog(@"[IAP ObjC] ✅ Swift 辅助类已找到");
// 检查退款可用性
if ([swiftClass respondsToSelector:@selector(isRefundAvailable)]) {
BOOL available = [swiftClass isRefundAvailable];
NSLog(@"[IAP ObjC] 📱 退款功能可用性: %@", available ? @"是" : @"否");
}
// 检查 shared 单例方法
if ([swiftClass respondsToSelector:@selector(shared)]) {
id shared = [swiftClass shared];
NSLog(@"[IAP ObjC] ✅ Swift 单例获取成功: %@", shared);
}
} else {
NSLog(@"[IAP ObjC] ❌ Swift 辅助类未找到!请检查编译配置");
}
}
#pragma mark - 导出给 UniApp 的方法
// 导出 beginRefundRequest 方法给 UniApp 调用
UNI_EXPORT_METHOD(@selector(beginRefundRequest:callback:))
- (void)beginRefundRequest:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSLog(@"[IAP ObjC] ========================================");
NSLog(@"[IAP ObjC] 🚀 beginRefundRequest 被调用");
NSLog(@"[IAP ObjC] 📥 参数: %@", options);
NSLog(@"[IAP ObjC] ========================================");
// 1. 参数验证
if (!options || ![options isKindOfClass:[NSDictionary class]]) {
NSLog(@"[IAP ObjC] ❌ 参数无效: 不是字典类型");
if (callback) {
callback(@"error: invalid parameters", NO);
}
return;
}
// 2. 获取交易ID - UniApp 传递的是 NSString
id transactionIdObj = options[@"transactionId"];
if (!transactionIdObj) {
NSLog(@"[IAP ObjC] ❌ 缺少 transactionId 参数");
if (callback) {
callback(@"error: missing transactionId", NO);
}
return;
}
// 3. 转换交易ID为 UInt64
UInt64 transactionIdValue = 0;
if ([transactionIdObj isKindOfClass:[NSString class]]) {
// 字符串类型
NSString *transactionIdStr = (NSString *)transactionIdObj;
transactionIdValue = (UInt64)[transactionIdStr longLongValue];
NSLog(@"[IAP ObjC] 📝 交易ID(字符串): %@ -> %llu", transactionIdStr, transactionIdValue);
} else if ([transactionIdObj isKindOfClass:[NSNumber class]]) {
// 数字类型
transactionIdValue = [(NSNumber *)transactionIdObj unsignedLongLongValue];
NSLog(@"[IAP ObjC] 🔢 交易ID(数字): %@ -> %llu", transactionIdObj, transactionIdValue);
} else {
NSLog(@"[IAP ObjC] ❌ 交易ID类型错误: %@", [transactionIdObj class]);
if (callback) {
callback(@"error: transactionId must be string or number", NO);
}
return;
}
// 4. 验证交易ID有效性
if (transactionIdValue == 0) {
NSLog(@"[IAP ObjC] ❌ 无效的交易ID值: 0");
if (callback) {
callback(@"error: invalid transactionId value", NO);
}
return;
}
// 5. 获取当前活跃的 WindowScene
UIWindowScene *windowScene = [DCIapRefundPluginSwiftHelper getCurrentWindowScene];
if (!windowScene) {
NSLog(@"[IAP ObjC] ❌ 获取 WindowScene 失败");
if (callback) {
callback(@"error: no active window scene", NO);
}
return;
}
NSLog(@"[IAP ObjC] ✅ WindowScene 获取成功: %@", windowScene);
// 6. 获取 Swift 单例
DCIapRefundPluginSwiftHelper *swiftHelper = [DCIapRefundPluginSwiftHelper shared];
if (!swiftHelper) {
NSLog(@"[IAP ObjC] ❌ 获取 Swift Helper 失败");
if (callback) {
callback(@"error: failed to get swift helper", NO);
}
return;
}
// 7. 检查 Swift 方法是否存在
SEL refundSelector = NSSelectorFromString(@"beginRefundRequestWithTransactionID:windowScene:completion:");
if (![swiftHelper respondsToSelector:refundSelector]) {
NSLog(@"[IAP ObjC] ❌ Swift 退款方法不存在");
// 列出所有可用方法帮助调试
unsigned int methodCount = 0;
Method *methods = class_copyMethodList([swiftHelper class], &methodCount);
NSLog(@"[IAP ObjC] 📋 Swift 实例方法列表:");
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
NSString *methodName = NSStringFromSelector(selector);
NSLog(@"[IAP ObjC] - %@", methodName);
}
free(methods);
if (callback) {
callback(@"error: refund method not found", NO);
}
return;
}
// 8. 调用 Swift 方法(使用正确的方法名)
NSLog(@"[IAP ObjC] 📞 正在调用 Swift 退款方法...");
[swiftHelper beginRefundRequestWithTransactionID:transactionIdValue
windowScene:windowScene
completion:^(NSString * _Nullable result, NSError * _Nullable error) {
// 9. 处理回调结果
if (error) {
NSLog(@"[IAP ObjC] ❌ 退款失败: %@", error.localizedDescription);
if (callback) {
NSString *errorMsg = [NSString stringWithFormat:@"error: %@", error.localizedDescription];
callback(errorMsg, NO);
}
} else {
NSLog(@"[IAP ObjC] ✅ 退款结果: %@", result);
if (callback) {
callback(result, NO);
}
}
}];
NSLog(@"[IAP ObjC] ✅ beginRefundRequest 调用完成,等待回调...");
}
#pragma mark - 导出测试方法(用于验证插件是否正常)
UNI_EXPORT_METHOD(@selector(test:))
- (void)test:(UniModuleKeepAliveCallback)callback {
NSLog(@"[IAP ObjC] 🧪 test 方法被调用");
NSDictionary *info = @{
@"status": @"ok",
@"message": @"IapRefundModule is working",
@"timestamp": @([[NSDate date] timeIntervalSince1970]),
@"refundAvailable": @([DCIapRefundPluginSwiftHelper isRefundAvailable])
};
if (callback) {
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:info options:0 error:&error];
if (jsonData) {
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
callback(jsonString, NO);
} else {
callback(@"test success", NO);
}
}
}
#pragma mark - 导出获取退款可用性方法
UNI_EXPORT_METHOD(@selector(isRefundAvailable:))
- (void)isRefundAvailable:(UniModuleKeepAliveCallback)callback {
BOOL available = [DCIapRefundPluginSwiftHelper isRefundAvailable];
NSLog(@"[IAP ObjC] ℹ️ 退款可用性: %@", available ? @"是" : @"否");
if (callback) {
callback(available ? @"available" : @"unavailable", NO);
}
}
@end
```
Uniapp页面如下:
```vue
<template>
<view class="container">
<!-- 头部标题 -->
<view class="header">
<text class="title">Apple 内购退款测试</text>
<text class="subtitle">iOS 15+ 原生退款弹窗</text>
</view>
<!-- 交易ID输入区域 -->
<view class="card">
<view class="card-header">
<text class="card-title">交易信息</text>
<text class="card-badge">必填</text>
</view>
<view class="input-group">
<text class="label">交易ID (transactionId)</text>
<input class="input" type="number" v-model="transactionId" placeholder="请输入苹果返回的交易ID"
placeholder-style="color: #999;" />
<text class="tip">格式:1234567890123456</text>
</view>
<view class="input-group" v-if="showProductId">
<text class="label">产品ID (可选)</text>
<input class="input" type="text" v-model="productId" placeholder="请输入产品ID"
placeholder-style="color: #999;" />
</view>
</view>
<!-- 测试按钮区域 -->
<view class="card">
<view class="card-header">
<text class="card-title">操作</text>
</view>
<!-- 主要操作按钮 -->
<view class="button-group">
<button class="button primary" :disabled="!isValidTransactionId || isLoading" @click="handleRefund">
<text v-if="!isLoading">💰 发起退款申请</text>
<text v-else>⏳ 处理中...</text>
</button>
<button class="button secondary" @click="clearInput" :disabled="isLoading">
🗑️ 清空输入
</button>
</view>
<!-- 快捷测试数据 -->
<view class="quick-test">
<text class="quick-title">快捷测试:</text>
<view class="quick-buttons">
<text class="quick-tag" @click="setTestId('2200001111100000')">2200001111100000</text>
</view>
</view>
</view>
<!-- 系统信息卡片 -->
<view class="card">
<view class="card-header">
<text class="card-title">系统信息</text>
</view>
<view class="info-item">
<text class="info-label">平台:</text>
<text class="info-value">{{ platform }}</text>
<text :class="['badge', isIOS ? 'badge-ios' : 'badge-other']">
{{ isIOS ? 'iOS' : '非iOS' }}
</text>
</view>
<view class="info-item">
<text class="info-label">系统版本:</text>
<text class="info-value">{{ systemVersion }}</text>
<text :class="['badge', isIOS15Plus ? 'badge-success' : 'badge-warning']">
{{ isIOS15Plus ? '支持退款' : '不支持退款' }}
</text>
</view>
<view class="info-item">
<text class="info-label">插件状态:</text>
<text class="info-value">{{ pluginStatus }}</text>
<text :class="['badge', hasPlugin ? 'badge-success' : 'badge-error']">
{{ hasPlugin ? '已加载' : '未加载' }}
</text>
</view>
</view>
<!-- 历史记录 -->
<view class="card" v-if="history.length > 0">
<view class="card-header">
<text class="card-title">请求历史</text>
<text class="card-clear" @click="clearHistory">清空</text>
</view>
<view class="history-list">
<view class="history-item" v-for="(item, index) in history" :key="index"
:class="getHistoryItemClass(item.status)">
<view class="history-time">{{ item.time }}</view>
<view class="history-content">
<text class="history-id">交易ID: {{ item.transactionId }}</text>
<text :class="['history-status', '' + item.status]">
{{ getStatusText(item.status) }}
</text>
</view>
<text v-if="item.message" class="history-message">{{ item.message }}</text>
</view>
</view>
</view>
<!-- 结果弹窗 -->
<view class="modal" v-if="showResult">
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">{{ resultTitle }}</text>
<text class="modal-close" @click="showResult = false">✕</text>
</view>
<view class="modal-body">
<view :class="['modal-icon', resultIconClass]">
{{ resultIcon }}
</view>
<text class="modal-message">{{ resultMessage }}</text>
<view v-if="resultDetail" class="modal-detail">
{{ resultDetail }}
</view>
</view>
<view class="modal-footer">
<button class="modal-button" @click="showResult = false">确 定</button>
</view>
</view>
</view>
<!-- 加载遮罩 -->
<view class="loading-mask" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">正在请求退款...</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// 输入数据
transactionId: '',
productId: '',
// UI状态
isLoading: false,
showResult: false,
showProductId: false, // 是否显示产品ID输入
// 结果数据
resultTitle: '',
resultMessage: '',
resultDetail: '',
resultIcon: '',
resultIconClass: '',
// 系统信息
platform: '',
systemVersion: '',
isIOS: false,
isIOS15Plus: false,
hasPlugin: false,
pluginStatus: '检测中...',
// 历史记录
history: []
}
},
computed: {
// 验证交易ID是否有效(16位数字或特定格式)
isValidTransactionId() {
return this.transactionId && /^\d{1,16}$/.test(this.transactionId)
}
},
onLoad() {
this.checkSystemInfo()
this.checkPlugin()
this.loadHistory()
},
methods: {
// 检查系统信息
checkSystemInfo() {
// #ifdef APP-PLUS
uni.getSystemInfo({
success: (res) => {
this.platform = res.platform
this.systemVersion = res.system
this.isIOS = res.platform === 'ios'
// 检查iOS版本
if (this.isIOS) {
const version = parseFloat(res.system.split(' ')[1] || '0')
this.isIOS15Plus = version >= 15
}
}
})
// #endif
},
// 检查插件是否可用
checkPlugin() {
// #ifdef APP-PLUS
try {
const module = uni.requireNativePlugin('DCIapRefundPlugin')
if (module && typeof module.beginRefundRequest === 'function') {
this.hasPlugin = true
this.pluginStatus = 'IapRefundModule 已就绪'
console.log('✅ 退款插件加载成功')
} else {
this.hasPlugin = false
this.pluginStatus = '插件未找到或方法缺失'
}
} catch (e) {
console.error('❌ 插件加载失败:', e)
this.hasPlugin = false
this.pluginStatus = '插件加载失败: ' + e.message
}
// #endif
},
// 处理退款请求
// 修改 refund-test.vue 中的 handleRefund 方法
handleRefund() {
if (!this.isValidTransactionId) {
this.showErrorToast('请输入有效的交易ID')
return
}
this.isLoading = true
// 关键:确保交易ID是字符串类型
const params = {
transactionId: String(this.transactionId).trim()
}
console.log('🚀 发起退款请求,参数:', params)
// #ifdef APP-PLUS
try {
const iapRefundModule = uni.requireNativePlugin('DCIapRefundPlugin')
console.log('📱 插件对象:', iapRefundModule ? '获取成功' : '获取失败')
if (!iapRefundModule) {
this.isLoading = false
this.pluginStatus = '❌ 插件获取失败'
this.showResultDialog('错误', '❌', '退款插件未加载', 'error')
return
}
// 先调用 test 方法验证插件
if (typeof iapRefundModule.test === 'function') {
iapRefundModule.test((res) => {
console.log('🧪 插件测试结果:', res)
this.pluginStatus = '✅ 插件正常'
})
}
// 调用退款方法
iapRefundModule.beginRefundRequest(params, (result) => {
this.isLoading = false
console.log('📦 退款回调结果:', result)
this.handleRefundResult(result)
this.addHistoryRecord(result)
})
} catch (e) {
this.isLoading = false
console.error('❌ 调用异常:', e)
this.showResultDialog('错误', '❌', e.message, 'error')
}
// #endif
},
// 添加获取退款可用性方法
checkRefundAvailability() {
// #ifdef APP-PLUS
const module = uni.requireNativePlugin('DCIapRefundPlugin')
if (module && typeof module.isRefundAvailable === 'function') {
module.isRefundAvailable((res) => {
console.log('ℹ️ 退款可用性:', res)
this.isRefundAvailable = res === 'available'
})
}
// #endif
},
// 处理退款结果
handleRefundResult(result) {
if (typeof result === 'string') {
if (result === 'success') {
this.showResultDialog(
'退款申请成功',
'✅',
'已向Apple提交退款请求,请等待处理结果',
'success'
)
} else if (result === 'userCancelled') {
this.showResultDialog(
'已取消',
'ℹ️',
'用户取消了退款申请',
'info'
)
} else if (result && result.startsWith('error:')) {
this.showResultDialog(
'退款失败',
'❌',
result.replace('error:', '').trim(),
'error'
)
} else {
this.showResultDialog(
'未知状态',
'⚠️',
result || '未知返回结果',
'warning'
)
}
} else {
this.showResultDialog(
'错误',
'❌',
'无效的返回格式',
'error'
)
}
},
// 显示结果弹窗
showResultDialog(title, icon, message, type) {
this.resultTitle = title
this.resultIcon = icon
this.resultMessage = message
this.resultIconClass = type
this.showResult = true
// 同时显示Toast
uni.showToast({
title: message.length > 15 ? title : message,
icon: type === 'success' ? 'success' : 'none',
duration: 2000
})
},
// 显示错误Toast
showErrorToast(message) {
uni.showToast({
title: message,
icon: 'none',
duration: 2000
})
},
// 添加历史记录
addHistoryRecord(result) {
const record = {
time: this.formatTime(new Date()),
transactionId: this.transactionId,
status: this.getResultStatus(result),
message: typeof result === 'string' ? result : JSON.stringify(result),
productId: this.productId || '-'
}
this.history.unshift(record)
// 只保留最近10条
if (this.history.length > 10) {
this.history.pop()
}
// 保存到Storage
uni.setStorageSync('refund_history', this.history)
},
// 获取结果状态
getResultStatus(result) {
if (typeof result === 'string') {
if (result === 'success') return 'success'
if (result === 'userCancelled') return 'cancel'
if (result.startsWith('error:')) return 'error'
}
return 'unknown'
},
// 获取状态文本
getStatusText(status) {
const map = {
'success': '成功',
'cancel': '取消',
'error': '失败',
'unknown': '未知'
}
return map[status] || status
},
// 获取历史记录项样式
getHistoryItemClass(status) {
return `history-item-${status}`
},
// 加载历史记录
loadHistory() {
try {
const history = uni.getStorageSync('refund_history')
if (history) {
this.history = history
}
} catch (e) {
console.error('加载历史记录失败:', e)
}
},
// 清空历史记录
clearHistory() {
uni.showModal({
title: '确认清空',
content: '确定要清空所有历史记录吗?',
success: (res) => {
if (res.confirm) {
this.history = []
uni.removeStorageSync('refund_history')
uni.showToast({
title: '已清空',
icon: 'success'
})
}
}
})
},
// 设置测试ID
setTestId(id) {
this.transactionId = id
},
// 清空输入
clearInput() {
this.transactionId = ''
this.productId = ''
},
// 格式化时间
formatTime(date) {
const hh = date.getHours().toString().padStart(2, '0')
const mm = date.getMinutes().toString().padStart(2, '0')
const ss = date.getSeconds().toString().padStart(2, '0')
return `${hh}:${mm}:${ss}`
}
}
}
</script>
<style scoped>
.container {
padding: 30rpx;
min-height: 100vh;
box-sizing: border-box;
}
/* 头部样式 */
.header {
margin-bottom: 30rpx;
text-align: center;
}
.title {
font-size: 44rpx;
font-weight: 700;
color: #1a1a1a;
display: block;
}
.subtitle {
font-size: 28rpx;
color: #666;
margin-top: 12rpx;
display: block;
}
/* 卡片样式 */
.card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.card-badge {
font-size: 22rpx;
color: #ff6b6b;
background: #fff1f0;
padding: 6rpx 16rpx;
border-radius: 30rpx;
}
.card-clear {
font-size: 26rpx;
color: #1890ff;
padding: 10rpx;
}
/* 输入组样式 */
.input-group {
margin-bottom: 30rpx;
}
.label {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
.input {
width: 100%;
height: 88rpx;
background: #f8f9fc;
border: 2rpx solid #e9ecef;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.input:focus {
border-color: #007aff;
background: #fff;
}
.tip {
font-size: 24rpx;
color: #999;
margin-top: 12rpx;
display: block;
}
/* 按钮样式 */
.button-group {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
}
.button {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
border: none;
}
.button.primary {
background: linear-gradient(135deg, #007aff, #0051d5);
color: white;
}
.button.primary:disabled {
background: #cce4ff;
color: #999;
}
.button.secondary {
background: #f8f9fc;
color: #666;
border: 2rpx solid #e9ecef;
}
/* 快捷测试 */
.quick-test {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.quick-title {
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
display: block;
}
.quick-buttons {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.quick-tag {
font-size: 26rpx;
color: #007aff;
background: #f0f8ff;
padding: 12rpx 24rpx;
border-radius: 40rpx;
border: 2rpx solid #b8e0ff;
}
/* 系统信息 */
.info-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
font-size: 28rpx;
}
.info-label {
color: #666;
width: 160rpx;
}
.info-value {
color: #333;
flex: 1;
}
.badge {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 30rpx;
margin-left: 20rpx;
}
.badge-ios {
background: #e6f7ff;
color: #1890ff;
}
.badge-other {
background: #f5f5f5;
color: #999;
}
.badge-success {
background: #f6ffed;
color: #52c41a;
}
.badge-warning {
background: #fff7e6;
color: #fa8c16;
}
.badge-error {
background: #fff2f0;
color: #ff4d4f;
}
/* 历史记录 */
.history-list {
max-height: 600rpx;
overflow-y: auto;
}
.history-item {
padding: 24rpx;
background: #fafafa;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
.history-item-success {
border-left: 8rpx solid #52c41a;
}
.history-item-cancel {
border-left: 8rpx solid #faad14;
}
.history-item-error {
border-left: 8rpx solid #ff4d4f;
}
.history-item-unknown {
border-left: 8rpx solid #999;
}
.history-time {
font-size: 24rpx;
color: #999;
margin-bottom: 12rpx;
}
.history-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.history-id {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.history-status {
font-size: 26rpx;
padding: 6rpx 16rpx;
border-radius: 30rpx;
}
.status-success {
background: #f6ffed;
color: #52c41a;
}
.status-cancel {
background: #fff7e6;
color: #fa8c16;
}
.status-error {
background: #fff2f0;
color: #ff4d4f;
}
.history-message {
font-size: 24rpx;
color: #666;
}
/* 模态框 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 600rpx;
background: white;
border-radius: 24rpx;
overflow: hidden;
}
.modal-header {
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2rpx solid #f0f0f0;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.modal-close {
font-size: 36rpx;
color: #999;
padding: 10rpx;
}
.modal-body {
padding: 40rpx;
text-align: center;
}
.modal-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.modal-message {
font-size: 30rpx;
color: #333;
line-height: 1.5;
}
.modal-detail {
font-size: 26rpx;
color: #666;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.modal-footer {
padding: 30rpx;
border-top: 2rpx solid #f0f0f0;
}
.modal-button {
height: 88rpx;
background: #007aff;
color: white;
border-radius: 44rpx;
font-size: 30rpx;
}
/* 加载遮罩 */
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.loading-content {
background: white;
padding: 40rpx 60rpx;
border-radius: 20rpx;
text-align: center;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 6rpx solid #f0f0f0;
border-top-color: #007aff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #333;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
```
编译好的插件在附件中,解压到根目录下的nativeplugins目录,保证解压后的package.json在nativeplugins\DCIapRefundPlugin\package.json处后生成个自定义打包基座运行就行了。注意,服务器回调退款会有一定延时,申请退款后要等几分钟才能收到回调,如果使用沙盒账号退款提示类似超时重试的内容的话,你需要打开系统设置,滚动到最底部找到开发者,然后滚动到最底部找到“沙盒Apple账户”点它后点管理,登录一下这个沙盒账户后回到app再操作就可以了