InAppPurchase
apple Developer console 에 앱 등록이 되어 있다는 가정하에 설명한다.
우선 인앱상품등록 부터 준비한다. (앱 업로드는 하지 않아도 샌드박스 결제테스트가 가능하다.)
인앱 상품 등록#
- 소스코드단에서 제품 ID 를 사용하기 때문에 잘 정해주면 된다.
- 나머지는 수정이 가능하나 제품 ID 는 변경이 안된다.
- 등록된 상품은 수정 및 삭제가 가능하다.
- 상품이 등록되었다면 앱 업로드 또는 출시를 하지 않아도 결제테스트 준비가 끝난 것이다.
샌드박스 테스터 추가#
- 샌드박스 결제테스트를 위해 테스터 계정을 추가한다.
- App Store Connect > 사용자 및 액서스 > Sandbox > 테스터 > 신규 사용자 등록
- 이메일은 애플계정에 등록되지 않은 이메일주소여야 한다.
인앱 결제 기능 활성화#
- 프로젝트 Targets > Signing & Capabilities 탭에 가면 + Capability 버튼 클릭
- In-App Purchase 활성화를 시켜주면 자동으로 Storekit 라이브러리도 포함된다.
여기까지 인앱결제 개발을 위한 준비가 끝났다.
코드 구현#
- setupBilling 에 NSArray (상품ID 목록) 을 넣어주면 인앱결제 모듈이 초기화된다.
- 결제가 완료된 후에는 따로 델레게이트나 block 으로 콜백을 만들어서 사용하면 된다.
BillingManager.h
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface BillingManager : NSObject<SKProductsRequestDelegate,SKPaymentTransactionObserver>
{
NSString *productID;
}
+ (instancetype)getInstance;
@property (nonatomic) NSArray *productList;
- (void)setupBilling:(NSArray*)arr;
- (void)purchase:(NSString*)pid;
- (BOOL)canMakePurchases;
- (float)getPrice:(NSString*)pid;
- (NSString*)getPriceCurrencyCode:(NSString*)pid;
- (SKProduct*)getSKProduct:(NSString*)pid;
@end
NS_ASSUME_NONNULL_END
BillingManager.m
#import "BillingManager.h"
@implementation BillingManager
+ (instancetype)getInstance {
static BillingManager *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[BillingManager alloc] init];
});
return shared;
}
- (void)setupBilling:(NSArray *)arr
{
_productList = arr;
NSMutableArray* tempArr = [NSMutableArray array];
for(RespAppInApp *item in _productList)
{
[tempArr addObject:item.ProductID];
}
NSSet *productIdentifiers = [NSSet setWithArray:tempArr];
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
}
- (float)getPrice:(NSString *)pid
{
for(SKProduct *skProduct in _productList)
{
if ([skProduct.productIdentifier isEqualToString:pid])
{
return [skProduct.price floatValue];
}
}
return 0;
}
- (NSString*)getPriceCurrencyCode:(NSString *)pid
{
for(SKProduct *skProduct in _productList)
{
if ([skProduct.productIdentifier isEqualToString:pid])
{
return skProduct.priceLocale.currencyCode;
}
}
return @"";
}
- (SKProduct*)getSKProduct:(NSString *)pid
{
for(SKProduct *item in _productList)
{
if([item.productIdentifier isEqualToString:pid] == YES)
return item;
}
return nil;
}
- (BOOL)canMakePurchases {
return [SKPaymentQueue canMakePayments];
}
- (void)purchase:(NSString *)pid
{
productID = pid;
if (_productList != nil)
{
if ([self canMakePurchases])
{
SKProduct *skProduct = [self getSKProduct:pid];
if(skProduct != nil)
{
SKPayment *skPayment = [SKPayment paymentWithProduct:skProduct];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] addPayment:skPayment];
}else{
NSLog(@"Product is null");
}
} else {
NSLog(@"No Purchases");
}
}
}
- (NSString *)getReceiptCode
{
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if (!receipt) {
NSLog(@"no receipt");
return nil;
} else {
// Get the receipt in encoded format
NSString *encodedReceipt = [receipt base64EncodedStringWithOptions:0];
return encodedReceipt;
}
}
#pragma mark - StoreKit Delegate
- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
NSLog(@"Purchasing");
break;
case SKPaymentTransactionStatePurchased:
{
long long milliseconds = (long long)(transaction.transactionDate.timeIntervalSince1970 * 1000.0);
NSString *purchaseTime = [NSString stringWithFormat:@"%lld", milliseconds];
NSLog(@"%lld", milliseconds);
NSLog(@"purchaseTime : %@", purchaseTime);
NSLog(@"transactionIdentifier : %@", transaction.transactionIdentifier);
NSLog(@"productIdentifier : %@", transaction.payment.productIdentifier);
NSLog(@"quantity : %@", [@(transaction.payment.quantity) stringValue]);
NSLog(@"getReceiptCode : %@", [self getReceiptCode]);
NSLog(@"Purchase completed");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
case SKPaymentTransactionStateRestored:
NSLog(@"Restored");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
NSLog(@"Purchase failed %@", transaction.error);
//[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateDeferred:
{
break;
}
default:
break;
}
}
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSUInteger count = [response.products count];
if (count > 0) {
_productList = response.products;
for(SKProduct *skProduct in _productList)
{
NSLog(@"Found product: %@ – Product: %@ – Price: %0.2f", skProduct.productIdentifier, skProduct.localizedTitle, skProduct.price.floatValue);
}
} else {
NSLog(@"No product or complete contract and tax financial transaction items.");
}
}
@end
영수증 검증#
- 서버가 없다면 클라이언트에서 구현해도 된다.
- 클라나 서버 둘중 한군데서 처리하면 되고 방식은 동일하다.
- 결제처리가 완료된 후 호출
- 검증url은 샌드박스용 앱스토어용 두가지가 있다.
-(void)verifyReceipt
{
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if (!receipt) {
NSLog(@"no receipt info");
}
// Create the JSON object that describes the request
NSError *error;
NSDictionary *requestContents = @{
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) {
NSLog(@"%@", error);
}
// Create a POST request with the receipt data.
// https://sandbox.itunes.apple.com/verifyReceipt , https://buy.itunes.apple.com/verifyReceipt
NSURL *storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
[[NSURLSession sharedSession] dataTaskWithRequest:storeRequest
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error)
{
if(error)
{
NSLog(@"%@", error);
}else{
NSError *error;
NSDictionary *respDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!respDict) {
NSLog(@"%@", error);
}
NSLog(@"%@", respDict);
}
}];
}