DYFStoreKit
A lightweight and easy-to-use iOS library for In-App Purchases. (Objective-C)
DYFStoreKit
uses blocks and notifications to wrap StoreKit
, provides receipt verification and transaction persistence.
相关链接
- DYFRuntimeProvider
- DYFKeychain
- DYFStoreReceiptVerifier
- Unity-iOS-InAppPurchase
- iOS 内购程序完整编程指南
- 如何轻松完成 iOS 内购配置
特性
- 超简单的内购。
- 内置对已购内容的记忆支持。
- 内置收据验证(远程)。
- 内置托管内容下载和通知。
组(ID:614799921)
安装
使用 CocoaPods
pod 'DYFStoreKit'
or
pod 'DYFStoreKit', '~> 2.0.2'
查看wiki,获取更多选项。
使用方法
接下来我会展示如何使用 DYFStoreKit
。
初始化
初始化如下。
- 添加交易观察者并监控交易变化。
- 实例化数据持久化对象并存储与交易相关的信息。
- 遵循协议
DYFStoreAppStorePaymentDelegate
,处理从 App Store 购买的产品付款。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self initIAPSDK];
return YES;
}
- (void)initIAPSDK
{
[DYFStoreManager.shared addStoreObserver];
// Adds an observer that responds to updated transactions to the payment queue.
// If an application quits when transactions are still being processed, those transactions are not lost. The next time the application launches, the payment queue will resume processing the transactions. Your application should always expect to be notified of completed transactions.
// If more than one transaction observer is attached to the payment queue, no guarantees are made as to the order they will be called in. It is recommended that you use a single observer to process and finish the transaction.
[DYFStore.defaultStore addPaymentTransactionObserver];
// Sets the delegate processes the purchase which was initiated by user from the App Store.
DYFStore.defaultStore.delegate = self;
}
您可以使用 DYFStoreAppStorePaymentDelegate
协议处理用户从 App Store 发起的购买,并实现自己的接口。
// Processes the purchase which was initiated by user from the App Store.
- (void)didReceiveAppStorePurchaseRequest:(SKPaymentQueue *)queue payment:(SKPayment *)payment forProduct:(SKProduct *)product
{
if (![DYFStore canMakePayments]) {
[self showTipsMessage:@"Your device is not able or allowed to make payments!"];
return;
}
// Get account name from your own user system.
NSString *accountName = @"Handsome Jon";
// This algorithm is negotiated with server developer.
NSString *userIdentifier = DYFStore_supplySHA256(accountName);
DYFStoreLog(@"userIdentifier: %@", userIdentifier);
[DYFStoreManager.shared addPayment:product.productIdentifier userIdentifier:userIdentifier];
}
请求产品
在请求产品之前,您需要检查设备是否可以或被允许进行支付。
if (![DYFStore canMakePayments]) {
[self showTipsMessage:@"Your device is not able or allowed to make payments!"];
return;
}
要开始购买流程,您的应用程序必须知道其产品标识符。有两种从 App Store 获取产品信息的方法。
策略 1: 您的应用程序可以使用产品标识符获取 App Store 中可供销售的产品信息,并直接提交支付请求。
- (IBAction)fetchesProductAndSubmitsPayment:(id)sender
{
// You need to check whether the device is not able or allowed to make payments before requesting product.
if (![DYFStore canMakePayments]) {
[self showTipsMessage:@"Your device is not able or allowed to make payments!"];
return;
}
[self showLoading:@"Loading..."];
NSString *productId = @"com.hncs.szj.coin48";
[DYFStore.defaultStore requestProductWithIdentifier:productId success:^(NSArray *products, NSArray *invalidIdentifiers) {
[self hideLoading];
if (products.count == 1) {
NSString *productId = ((SKProduct *)products[0]).productIdentifier;
[self addPayment:productId];
} else {
[self showTipsMessage:@"There is no this product for sale!"];
}
} failure:^(NSError *error) {
[self hideLoading];
NSString *value = error.userInfo[NSLocalizedDescriptionKey];
NSString *msg = value ?: error.localizedDescription;
// This indicates that the product cannot be fetched, because an error was reported.
[self sendNotice:[NSString stringWithFormat:@"An error occurs, %zi, %@", error.code, msg]];
}];
}
- (void)addPayment:(NSString *)productId
{
// Get account name from your own user system.
NSString *accountName = @"Handsome Jon";
// This algorithm is negotiated with server developer.
NSString *userIdentifier = DYFStore_supplySHA256(accountName);
DYFStoreLog(@"userIdentifier: %@", userIdentifier);
[DYFStoreManager.shared addPayment:productId userIdentifier:userIdentifier];
}
策略 2:它可以从 App Store 中检索产品信息,并向用户提供其商店 UI。在您应用程序中销售的产品都有一个唯一的识别符。您应用程序使用这些产品识别符来获取 App Store 中上市产品的信息,例如定价,以及当用户购买这些产品时提交付款请求。
- (NSArray *)fetchProductIdentifiersFromServer
{
NSArray *productIds = @[@"com.hncs.szj.coin42", // 42 gold coins for ¥6.
@"com.hncs.szj.coin210", // 210 gold coins for ¥30.
@"com.hncs.szj.coin686", // 686 gold coins for ¥98.
@"com.hncs.szj.coin1386", // 1386 gold coins for ¥198.
@"com.hncs.szj.coin2086", // 2086 gold coins for ¥298.
@"com.hncs.szj.coin4886", // 4886 gold coins for ¥698.
@"com.hncs.szj.vip1", // non-renewable vip subscription for a month.
@"com.hncs.szj.vip2" // Auto-renewable vip subscription for three months.
];
return productIds;
}
- (IBAction)fetchesProductsFromAppStore:(id)sender
{
// You need to check whether the device is not able or allowed to make payments before requesting products.
if (![DYFStore canMakePayments]) {
[self showTipsMessage:@"Your device is not able or allowed to make payments!"];
return;
}
[self showLoading:@"Loading..."];
NSArray *productIds = [self fetchProductIdentifiersFromServer];
[DYFStore.defaultStore requestProductWithIdentifiers:productIds success:^(NSArray *products, NSArray *invalidIdentifiers) {
[self hideLoading];
if (products.count > 0) {
[self processData:products];
} else if (products.count == 0 && invalidIdentifiers.count > 0) {
// Please check the product information you set up.
[self showTipsMessage:@"There are no products for sale!"];
}
} failure:^(NSError *error) {
[self hideLoading];
NSString *value = error.userInfo[NSLocalizedDescriptionKey];
NSString *msg = value ?: error.localizedDescription;
// This indicates that the products cannot be fetched, because an error was reported.
[self sendNotice:[NSString stringWithFormat:@"An error occurs, %zi, %@", error.code, msg]];
}];
}
- (void)processData:(NSArray *)products
{
NSMutableArray *modelArray = [NSMutableArray arrayWithCapacity:0];
for (SKProduct *product in products) {
DYFStoreProduct *p = [[DYFStoreProduct alloc] init];
p.identifier = product.productIdentifier;
p.name = product.localizedTitle;
p.price = [product.price stringValue];
p.localePrice = [DYFStore.defaultStore localizedPriceOfProduct:product];
p.localizedDescription = product.localizedDescription;
[modelArray addObject:p];
}
[self displayStoreUI:modelArray];
}
- (void)displayStoreUI:(NSMutableArray *)dataArray
{
DYFStoreViewController *storeVC = [[DYFStoreViewController alloc] init];
storeVC.dataArray = dataArray;
[self.navigationController pushViewController:storeVC animated:YES];
}
添加支付
请求支付具有给定产品标识符的产品。
[DYFStore.defaultStore purchaseProduct:@"com.hncs.szj.coin210"];
如果您需要为系统中的用户账户添加付款的不可见标识符,您可以使用用户账户名称的单向哈希值来计算此属性的值。
计算 SHA256 哈希函数
CG_INLINE NSString *DYFStore_supplySHA256(NSString *string)
{
const int digestLength = CC_SHA256_DIGEST_LENGTH; // 32
unsigned char md[digestLength];
const char *cStr = [string UTF8String];
size_t cStrLen = strlen(cStr);
// Confirm that the length of C string is small enough
// to be recast when calling the hash function.
if (cStrLen > UINT32_MAX) {
NSLog(@"C string too long to hash: %@", string);
return nil;
}
CC_SHA256(cStr, (CC_LONG)cStrLen, md);
// Convert the array of bytes into a string showing its hex represention.
NSMutableString *hash = [NSMutableString string];
for (int i = 0; i < digestLength; i++) {
// Add a dash every four bytes, for readability.
if (i != 0 && i%4 == 0) {
//[hash appendString:@"-"];
}
[hash appendFormat:@"%02x", md[i]];
}
return hash;
}
请求支付给带有用户系统账户不可见标识符的指定产品。
[DYFStore.defaultStore purchaseProduct:@"com.hncs.szj.coin210" userIdentifier:@"A43512564ACBEF687924646CAFEFBDCAEDF4155125657"];
恢复交易
- 不使用用户账户标识符恢复交易。
[DYFStore.defaultStore restoreTransactions];
- 使用用户账户标识符恢复交易。
[DYFStore.defaultStore restoreTransactions:@"A43512564ACBEF687924646CAFEFBDCAEDF4155125657"];
刷新收据
如果 Bundle.main.appStoreReceiptURL
是 null,则需要创建刷新收据请求以获取付款交易的收据。
[DYFStore.defaultStore refreshReceiptOnSuccess:^{
[self storeReceipt];
} failure:^(NSError *error) {
[self failToRefreshReceipt];
}];
通知
DYFStoreKit
发送与 StoreKit
相关事件的_notification_,并将 NSNotification
扩展以提供相关信息。要接收它们,请将观察员添加到 DYFStoreKit
管理器。
添加商店观察者
- (void)addStoreObserver
{
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(processPurchaseNotification:) name:DYFStorePurchasedNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(processDownloadNotification:) name:DYFStoreDownloadedNotification object:nil];
}
删除商店观察者
当应用程序退出时,您需要删除商店观察者。
- (void)removeStoreObserver
{
[NSNotificationCenter.defaultCenter removeObserver:self name:DYFStorePurchasedNotification object:nil];
[NSNotificationCenter.defaultCenter removeObserver:self name:DYFStoreDownloadedNotification object:nil];
}
支付交易通知
支付请求后或每次恢复交易后都会发送支付交易通知。
- (void)processPurchaseNotification:(NSNotification *)notification
{
[self hideLoading];
self.purchaseInfo = notification.object;
switch (self.purchaseInfo.state) {
case DYFStorePurchaseStatePurchasing:
[self showLoading:@"Purchasing..."];
break;
case DYFStorePurchaseStateCancelled:
[self sendNotice:@"You cancel the purchase"];
break;
case DYFStorePurchaseStateFailed:
[self sendNotice:[NSString stringWithFormat:@"An error occurred, %zi", self.purchaseInfo.error.code]];
break;
case DYFStorePurchaseStateSucceeded:
case DYFStorePurchaseStateRestored:
[self completePayment];
break;
case DYFStorePurchaseStateRestoreFailed:
[self sendNotice:[NSString stringWithFormat:@"An error occurred, %zi", self.purchaseInfo.error.code]];
break;
case DYFStorePurchaseStateDeferred:
DYFStoreLog(@"Deferred");
break;
default:
break;
}
}
下载通知
- (void)processDownloadNotification:(NSNotification *)notification
{
self.downloadInfo = notification.object;
switch (self.downloadInfo.downloadState) {
case DYFStoreDownloadStateStarted:
DYFStoreLog(@"The download started");
break;
case DYFStoreDownloadStateInProgress:
DYFStoreLog(@"The download progress: %.2f%%", self.downloadInfo.downloadProgress);
break;
case DYFStoreDownloadStateCancelled:
DYFStoreLog(@"The download cancelled");
break;
case DYFStoreDownloadStateFailed:
DYFStoreLog(@"The download failed");
break;
case DYFStoreDownloadStateSucceeded:
DYFStoreLog(@"The download succeeded: 100%%");
break;
default:
break;
}
}
收据验证
DYFStoreKit
默认不执行收据验证,但提供参考实现。您可以实现自己的自定义验证或使用库提供的参考验证器。
以下概述了参考验证器。有关更多信息,请参考Wiki。
参考验证器
您可以通过惰性加载来创建和返回收据验证器(DYFStoreReceiptVerifier
)。
- (DYFStoreReceiptVerifier *)receiptVerifier
{
if (!_receiptVerifier) {
_receiptVerifier = [[DYFStoreReceiptVerifier alloc] init];
_receiptVerifier.delegate = self;
}
return _receiptVerifier;
}
收据验证器委托收据验证,使您能够使用DYFStoreReceiptVerifierDelegate
协议提供自己的实现。
- (void)verifyReceiptDidFinish:(nonnull DYFStoreReceiptVerifier *)verifier didReceiveData:(nullable NSDictionary *)data {}
- (void)verifyReceipt:(nonnull DYFStoreReceiptVerifier *)verifier didFailWithError:(nonnull NSError *)error {}
您现在可以开始验证内购收据。
// Fetches the data of the bundle’s App Store receipt.
NSData *data = receiptData ?: [NSData dataWithContentsOfURL:DYFStore.receiptURL];
DYFStoreLog(@"data: %@", data);
[_receiptVerifier verifyReceipt:data];
// Only used for receipts that contain auto-renewable subscriptions.
//[_receiptVerifier verifyReceipt:data sharedSecret:@"A43512564ACBEF687924646CAFEFBDCAEDF4155125657"];
如果安全问题很重要,您可能希望避免使用开源的验证逻辑,而是提供自己的自定义验证器。
最好使用自己的服务器来获取客户端上传到应用程序商店服务器的参数,以此来验证商店服务器的收据(C -> 上传参数 -> S -> 应用程序商店S -> S -> 接收并解析数据 -> C,C: 客户端,S: 服务器)。
完成交易
只有在客户端和服务器采用安全通信和数据加密,并且收到验证通过后,才能完成交易。这样我们可以避免刷新订单和破解内购。如果我们无法完成验证,希望 StoreKit
持续提醒我们还有未完成的交易。
[DYFStore.defaultStore finishTransaction:transaction];
交易持久化
DYFStoreKit
为存储交易至 NSUserDefaults
(DYFStoreUserDefaultsPersistence
)提供一个可选的参考实现。
在付款过程中客户端崩溃时,存储交易信息尤为重要。当 storekit 再次通知未完成的付款时,它直接从文件中取数据,并执行收据验证,直到交易完成。
存储交易
- (void)storeReceipt
{
DYFStoreLog();
NSURL *receiptURL = DYFStore.receiptURL;
NSData *data = [NSData dataWithContentsOfURL:receiptURL];
if (!data || data.length == 0) {
[self refreshReceipt];
return;
}
DYFStoreNotificationInfo *info = self.purchaseInfo;
DYFStoreUserDefaultsPersistence *persister = [[DYFStoreUserDefaultsPersistence alloc] init];
DYFStoreTransaction *transaction = [[DYFStoreTransaction alloc] init];
if (info.state == DYFStorePurchaseStateSucceeded) {
transaction.state = DYFStoreTransactionStatePurchased;
} else if (info.state == DYFStorePurchaseStateRestored) {
transaction.state = DYFStoreTransactionStateRestored;
}
transaction.productIdentifier = info.productIdentifier;
transaction.userIdentifier = info.userIdentifier;
transaction.transactionIdentifier = info.transactionIdentifier;
transaction.transactionTimestamp = info.transactionDate.timestamp;
transaction.originalTransactionTimestamp = info.originalTransactionDate.timestamp;
transaction.originalTransactionIdentifier = info.originalTransactionIdentifier;
transaction.transactionReceipt = data.base64EncodedString;
[persister storeTransaction:transaction];
[self verifyReceipt:data];
}
删除交易
DYFStoreNotificationInfo *info = self.purchaseInfo;
DYFStore *store = DYFStore.defaultStore;
DYFStoreUserDefaultsPersistence *persister = [[DYFStoreUserDefaultsPersistence alloc] init];
if (info.state == DYFStorePurchaseStateRestored) {
SKPaymentTransaction *transaction = [store extractRestoredTransaction:info.transactionIdentifier];
[store finishTransaction:transaction];
} else {
SKPaymentTransaction *transaction = [store extractPurchasedTransaction:info.transactionIdentifier];
// The transaction can be finished only after the client and server adopt secure communication and data encryption and the receipt verification is passed. In this way, we can avoid refreshing orders and cracking in-app purchase. If we were unable to complete the verification, we want `StoreKit` to keep reminding us that there are still outstanding transactions.
[store finishTransaction:transaction];
}
[persister removeTransaction:info.transactionIdentifier];
if (info.originalTransactionIdentifier) {
[persister removeTransaction:info.originalTransactionIdentifier];
}
要求
DYFStoreKit
需要 iOS 7.0
或更高版本和 ARC
。
演示
了解更多,请将此项目(git clone https://github.com/chenxing640/DYFStoreKit.git
)克隆到本地目录。
欢迎反馈
如果您发现任何问题,请创建一个问题。我会很乐意帮助您。