=================================
=================================
=================================
출처: http://digitanomad.blogspot.kr/2013/08/ios-in-app-purchase.html
1. 앱 스토어에 상품(Product) 등록하기
애플리케이션의 상점에서 제공하고자 하는 모든 상품은 iTunes Connect를 통해 앱 스토어에 등록되어 있어야 한다. 상품을 등록할 때 이름, 설명, 가격 등의 정보를 입력해야 한다.
그리고 상품을 식별할 때는 product identifier라 불리는 식별자를 사용한다. 애플리케이션의 Store Kit이 앱 스토어와 통신할 때, product identifier를 이용해 상품을 등록할 때 입력했던 정보(이름, 설명 등)들을 받아올 수 있다. 또한, 고객이 상품 구매를 원할 때도 애플리케이션에서 product identifier를 이용해 구매하고자 하는 상품을 식별한다.
앱 스토어가 지원하는 상품의 종류(type)는 다음과 같다.
(1) Consumable
사용자가 아이템을 필요로 할 때 마다 구매해야 되는 상품이다. 예를 들면, 1회 서비스(게임 아이템)이 있다.
(2) Non-consumable
(3) Auto-renewable subscriptions
(4) Free subscriptions
(5) Non-renewing subscriptions
우리는 Consumable 아이템을 구현할 것이기 때문에 나머지 설명은 생략한다. 자세한 설명은 iTunes Connect Developer Guide의 In-App Purchase 페이지에 상세히 설명되어 있다.
2. Feature Delivery
애플리케이션에서 사용자에게 상품을 제공하는 delivery mechanism은 애플리케이션의 디자인과 구현에 있어서 매우 중요하다. 사용자에게 상품을 전달할 때 사용하는 두 가지 기본 모델이 있다. 하나는 built-in(내장) 모델과 서버(server) 모델이다. 두 모델 모두 상점에서 제공하는 제품의 목록을 추적(track)할 수 있고 사용자가 구매한 상품을 성공적으로 전달할 수 있다.
보통 게임에서는, 사용자 데이터를 보관하기 위해 자체 서버를 많이 사용하므로 서버 모델만 살펴 보겠다.
(1) Server Product Model
서버 모델에서는 애플리케이션에 상품을 전달할 자체 서버를 두어야 한다. 그리고 서버 모델은 구독, 서비스와 컨텐츠에 적합하다. 왜냐하면, 이러한 상품들은 애플리케이션 bundle을 변경하지 않고 데이터로서 전달될 수 있기 때문이다. 예를 들자면, 게임에서 새로운 플레이 환경(퍼즐, 레벨)을 애플리케이션에 제공하는 경우가 있을 수 있다.
Store Kit은 서버 디자인이나 애플리케이션의 동작을 정의하지 않기 때문에, 애플리케이션과 자체 서버간의 모든 상호작용을 책임지고 직접 구현해야 한다. 게다가, Store Kit은 특정 사용자를 식별하기 위한 mechanism을 제공하지 않는다. 그래서 사용자들을 식별하기 위한 mechanism도 본인이 직접 구현해야 한다.
다음 그림은 Server Product Model이 어떻게 동작하는 지 살펴볼 수 있는 그림이다.
애플은 property list에 product identifier를 미리 입력해 놓는 것보다 서버에서 product identifier를 받아오는 방법을 추천한다. 이 방법을 이용하면, 애플리케이션을 별도로 업데이트 하지 않고도 새로운 상품을 추가할 수 있어서 상품 운영을 유연하게 할 수 있다.
서버 모델에서는, 애플리케이션에서 앱 스토어로부터 거래(transaction)와 연관된 서명된(signed) 영수증(receipt)을 받아 서버로 전송할 수 있다. 그러면 서버에서는 영수증을 인증하고 어떤 컨텐츠를 애플리케이션에 전달할 지 결정하면 된다. 이 과정은 Verifying Store Receipts에서 자세히 다뤄보겠다.
3. Adding a Store to Your Application
먼저, 프로젝트에 StoreKit.framework를 링크해야 한다. 링크하는 방법은 프로젝트 설정파일(*.xcodeproj) - Build Phases - Link Binary With Libraries 항목에서 + 버튼을 눌러서 StoreKit.framework를 찾아주면 된다.
프레임워크를 링크하면, 다음 단계에 따라 상점 기능을 구현한다.
(1) 상점에 어떤 형태의 상품을 판매하려고 하는 지 확인한다.
서비스 하려는 기능에 제약이 있을 수 있다. Store Kit은 애플리케이션이 자체적으로 패치가 되거나 애플리케이션에 추가 코드를 다운로드 하는 방법을 허용하지 않는다. 제품은 애플리케이션에 있는 코드로 동작하거나 서버에서 전달된 데이터 파일을 이용해야 한다. 만약, 소스 코드를 변경해야 하는 기능이 필요하다면, 애플리케이션 버전 업데이트를 해야한다.
(2) iTunes Connect에 각 제품을 등록한다.
(3) 결제가 가능한지 확인한다.
사용자가 설정에서 결제를 막아둔 상태일 수도 있다. 그래서 새로운 결제 요청을 하기 전에 결제가 가능한 상태인지 확인해야 한다. 애플리케이션에서 사용자에게 상점을 보여주기 전이나 아이템에 대한 구매 시도가 있기 전에 확인하는 것이 좋다
if ([SKPaymentQueue canMakePayments]) { // 사용자에게 상점을 보여준다. } else { // 사용자에게 결제가 가능한 상태가 아니라고 알려준다. } |
(4) 제품에 대한 정보를 얻어온다.
SKProductRequest 객체를 생성하고, 판매하려는 아이템의 product identifier로 초기화(initialize) 한다. 그 다음 request 객체에 delegate를 붙이고, start 메소드를 호출한다. 이렇게 하면 응답으로 유효한 product identifier에 한해 현지화된(localized) 제품 정보를 받아올 수 있다.
사용자가 원하는 아이템 결제 요청을 할 때, product 객체를 사용하므로 response에서 받은 valid product array는 별도의 reference에 보관하고 있어야 한다.
- (void) requestProductData { SKProductRequest *request = [[SKProductRequest alloc] initWithProductIdentifiers:[NSSet setWithObject: kMyFeatureIdentifier]]; request.delegate = self; [request start]; } - (void) productsRequest:(SKProductRequest *)request didReceiveResponse:(SKProductsResponse *)response { NSArray *myProducts = response.products; // 상점 UI를 myProducts 내용을 바탕으로 꾸민다. // product list reference를 저장한다. } |
(5) 사용자에게 제품을 보여줄 UI를 추가한다.
Store Kit은 user interface class를 제공하지 않는다. 고객에게 제품을 어떻게 보여줄 지는 스스로 해결해야 한다.
(6) Payment queue에 transaction observer를 등록한다.
애플리케이션에 transaction observer를 instantiate하고 payment queue에 추가해야 한다.
MyStoreObserver *observer = [[MyStoreObserver alloc] init]; [[SKPaymentQueue defaultQueue] addTransactionObserver:observer]; |
애플리케이션을 실행했을 때 observer를 추가하는 것이 좋다. 앱 스토어는 모든 transaction이 끝나기 전에 애플리케이션이 종료되어도 예약해놨던(queued) transaction들을 저장하고 있다. 그래서 애플리케이션이 종료되기 이전에 예약된 transaction을 다시 불러올 수 있다.
(7) MyStoreObserver에 paymentQueue:updatedTransactions 메소드를 구현한다.
observer의 paymentQueue:updatedTransactions: 메소드는 새로운 transaction이 생성되거나 업데이트 될 때마다 호출된다.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; default: break; } } } |
(8) observer는 사용자가 아이템을 성공적으로 구매했을 때 제품을 전달한다.
- (void) completeTransaction : (SKPaymentTransaction *)transaction { // 애플리케이션에 이 두가지 메소드를 구현해야 한다. [self recordTransaction:transaction]; [self provideContent:transaction.payment.productIdentifier]; // payment queue에서 transaction을 제거한다. [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } |
성공한 transaction은 transaction identifier 속성과 완료된 결제 내역이 담긴 transactionReceipt 속성을 가지고 있다. transaction에 대한 기록을 남겨두고 싶다면 이 정보들을 저장하면 된다. 만약 애플리케이션이 서버에서 전달한 컨텐츠를 사용한다면, receipt를 서버로 보낼 수 있고 앱 스토어로부터 올바른 transaction인 지 확인할 수 있다.
제품을 전달하고 나면, 애플리케이션은 transaction을 끝내기 위해 반드시 finishTransaction 메소드를 호출해야 한다. finishTransaction 메소드를 호출하면, payment queue에서 transaction이 제거된다. 제품이 제대로 전달되도록, finishTransaction 메소드를 호출하기 전에 제품을 전달하자.
(9) 복구된(restored) 구매(purchase)에 대한 transaction을 종료한다.
- (void) restoreTransaction : (SKPaymentTransaction *)transaction { [self recordTransaction: transaction]; [self provideContent: transaction.originalTransaction.payment.productIdentifier]; [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } |
이 순서는 아이템을 구매할 때와 유사하다. 복구된 구매는 다른 transaction identifier와 receipt를 포함하는 새로운 transaction을 제공한다. 이것도 마찬가지로 transaction에 대해 기록하고 싶다면, 이 정보들을 저장하면 된다. 그리고 transaction을 완료 할 때가 되면, 실제 결제 객체를 담고 있는 원본 transaction의 product identifier를 이용해 컨텐츠를 애플리케이션에 전달할 수 있다.
(10) 실패한 구매에 대한 transaction을 종료한다.
- (void) failedTransaction: (SKPaymentTransaction *)transaction { if (transaction.error.code != SKErrorPaymentCancelled) { // 에러 메시지를 보여준다. } [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } |
대개 사용자가 아이템 구매를 취소할 때 transaction이 실패한다. 애플리케이션에서 transaction이 왜 실패했는지는 error field를 통해 알아볼 수 있다.
실패한 결제에 대한 처리는 payment queue에서 transaction을 제거하는 것으로 충분하다. 만약 애플리케이션에서 사용자에게 에러 메시지를 보여주려고 한다면, 사용자가 구매를 취소했을 때 에러 메시지를 보여주지 않도록 신경써야 한다.
(11) 모든 것이 준비가 됐으면, 아이템을 구매하자.
SKProduct *selectedProduct = <#from the products response list#>; SKPayment *payment = [SKPayment paymentWithProduct:selectedProduct]; [[SKPaymentQueue defaultQueue] addPayment:payment]; |
상점에서 하나의 제품에 대해 여러개를 구매하는 기능을 제공한다면, quantity 속성을 set한 payment 객체를 생성할 수 있다.
SKProduct *selectedProduct = <#from the products response list#>; SKPayment *payment = [SKPayment paymentWithProduct:selectedProduct]; payment.quantity = 3; [[SKPaymentQueue defaultQueue] addPayment:payment]; |
4. Verifying Store Receipts
5. Testing a Store
개발하는 동안, 애플리케이션에서 구매가 정상적으로 진행되는 지 테스트해야 한다. 그러나, 애플리케이션을 테스트 하는 동안 비용이 청구되는 것은 원하지 않을 것이다. 그래서 애플에서는 실제 금융 거래 없이 애플리케이션을 테스트 할 수 있는 Sandbox를 제공한다.
참고로, Store Kit은 IOS Simulator에서 컨텐츠 다운로드를 제외한 나머지는 테스트 할 수 있다.
(1) Sandbox
Xcode에서 애플리케이션을 실행하면, Store Kit은 앱 스토어에 바로 접속하지 않는다. 대신에, 특수한 sandbox store에 접속한다. Sandbox는 앱 스토어의 구조를 사용하지만, 실제로 비용 청구가 일어나지는 않는 환경이다. 마치 결제가 정상적으로 진행된 것처럼, transaction을 반환한다. 그리고 Sandbox는 In-App Purchase 테스트만 할 수 있는 특수한 iTunes Connect 계정을 사용한다. 그래서 sandbox store에 일반 iTunes Connect 계정을 사용할 수 없다.
애플리케이션을 테스트하기 위해, iTunes Connect에서 특수한 테스트 계정을 만들어야 한다. 애플리케이션을 여러 나라에 판매하려고 한다면, 테스트 계정도 각 나라별로 만들어야 한다. 테스트 계정 생성에 대한 자세한 내용은iTunes Connect Developer Guide의 In-App-Purchase 부분을 살펴보면 된다.
(2) Sandbox에서 테스트하기
Sandbox에서 다음 단계에 따라 애플리케이션을 테스트한다.
1) 테스트 디바이스에 이미 로그인 되어있는 iTunes 계정을 로그아웃 한다.
애플리케이션을 테스트하기 전에, 일반 iTunes 계정을 반드시 로그아웃 해야한다. iTunes 계정을 로그아웃 하기 위해, 애플리케이션을 종료하고, 설정을 들어가서 Store icon을 클릭한다. 그 다음 현재 활성화된 계정을 로그아웃 하면 된다. 참고로, iOS 3.0에서는 애플리케이션 설정에서 상점 카테고리에 있다.
중요!!!!
설정에서 테스트 계정으로 로그인 하면 안된다. 만약 로그인 했다면, 테스트 계정은 Sandbox에서 사용할 수 없다.
2) 애플리케이션을 실행한다.
계정을 로그아웃 했으면, 설정을 닫고 애플리케이션을 실행한다. 우리가 만든 상점에서 구매를 하려고 하면, Store Kit에서 transaction을 승인하겠냐고 물어본다. 이 때 테스트 계정으로 로그인하고, 지불을 승인하면 된다. 실제로 비용 청구는 일어나지 않지만, 해당 아이템에 대한 지불한 것처럼, transaction이 성공적으로 완료된다.
(3) Sandbox에서 Receipts 확인하기
Sandbox에서도 receipts를 확인할 수 있다. Sandbox에서 받은 receipts를 확인하기 위한 코드는 실제 앱 스토어와 같다. 하지만 sandbox URL은 다음과 같이 다르게 입력해야 한다.
NSURL *sandboxStoreURL = [[NSURL alloc] initWithString: @"https://sandbox.itunes.apple.com/verifyReceipt"]; |
=================================
=================================
=================================
출처: http://cusingerdevstory.tistory.com/?page=9
In-App Purchase를 하게되면 결제의 종류가 여러가지가 나옵니다.
전 이걸 외우질 않아 볼때마다 헤깔리기도 하여 기록으로 남깁니다. ^^
. Consumable : 1회성 아이템으로 구매 (ex. 게임의 캐쉬나 골드같은것)
. Non-Consumable : 1회성이 아닌 한번 구매하면 영원히(?) 사용 가능한 아이템 (ex. 게임의 스테이지나 광고제거)
Restore 버튼을 필히 만들어야 리젝당하지 않습니다.
. Auto-Renewable Subscriptions (ARS) : 정기구매로 정해진 기간마다 자동으로 결제가 되는 아이템 (ex. 잡지의 정기구독?)
. Non-Renewing Subscription (NRS) : 기간제 아이템 (ex. 30일 이용권, 60일 이용권)
. Free Subscription : Newsstand(뉴스 가판대) 내에서 사용.
[아이템의 성격이 중요]
아이템의 성격에 따라 위의 결제 방식을 결정해야 합니다.
예를들어 캐쉬를 Non-Consumable로 처리를 하게되면 리젝 사유가 됩니다.
제 경우 Consumable로 캐쉬를 사서 그 캐쉬를 이용하여 기간제 아이템을 사서 리젝을 당했습니다. 이는 최종 결제 아이템의 성격에 따라 결제 상품을 등록하라는것으로 생각됩니다. 그리고 NRS의 경우 실제 결제 상품 등록시 기간을 입력하는 부분이 존재하지 않는데 이로인해 혼란이 발생할 수 있습니다. 하지만 아이템의 성격을 결정하는 것이라 생각한다면 개발자가 알아서 기간을 줘야하는것이지요.
=================================
=================================
=================================
Ray가 알림: 본 튜토리얼은 iOS 6 진수성찬의 세 번째 튜토리얼입니다. 우리는 iOS 6에 대한 이전 튜토리얼들을 업데이트하고 있습니다. 따라서 본 튜토리얼은 ARC, 스토리보드 그리고 iOS 6의 새로운 API등과 같은 최종 기능으로 업데이트 되었습니다.
본 튜토리얼의 일부는 우리의 새로운 책 iOS 6 튜토리얼의 인앱구매 시작하기 챕터에서 발췌되었습니다. 그 책에서 구현된 iOS Hangman이라는 앱은 여기에서 함께 구현해 볼 간단한 샘플앱과 약간 다르고, iOS 6 Hosted Downloads와 같은 더 많은 기능을 담고 있긴 합니다. 그냥 시식용이니 마음껏 드세요. :)
본 튜토리얼은 독립 소프트웨어 개발자이자 게이머이면서 사이트 관리자인 Ray Wenderlich에 의해 작성되었습니다.
iOS 개발자에게 좋은 것 중 하나는 유료, 광고를 포함한 무료, 인앱구매등, 앱으로 수익을 내기 위한 다양한 모델이 있다는 것이다
특히, 인앱구매는 여러가지 이유로 외면할 수 없는 선택이다.
- 단지 앱에 책정된 가격보다 더욱 많은 수익을 올릴 수 있다. 어떤 사용자들은 추가 컨첸츠에 많은 지불을 한다.
- 앱을 대부분의 사람들이 망설임 없이 다운로드 받도록 무료로 올릴 수 있다. 그리고 그 앱에 만족하면 그들은 추가 컨테츠를 구매할 수 있다.
- 앱을 처음 등록한 이후, 추후 추가 수익을 더 내기 위해 새로운 앱을 만들기 보다 그 앱에 추가 컨텐츠를 계속 등록할 수 있다.
인앱구매는 다양한 비지니스모델에 이용될 수 있다. 예컨데, Wild Fables의 경우 나는 3개의 스토리를 가진 무료앱을 만들었고 추가 컨텐츠는 인앱구매를 적용하였다. 그리고, 현재 업그레이드가 진행 중인 Battle Map의 경우 유료앱이지만 추가 컨텐츠 선택을 인앱구매로 적용하였다.
본 튜토리얼에서 우리는 앱에 포함된 로컬 컨텐츠를 인앱구매를 이용해 사용가능하도록(unlock) 하는 방법을 배우게 될 것이며 여러분은 인앱구매의 복잡한 비동기방식을 다룰 수 있게 될 것이다.
여러분이 iOS 프로그래밍 기본 개념에 친숙하다는 가정하에 진행하겠다. 만일 여러분이 iOS 개발 입문자라면 이 사이트에서 다른 튜토리얼들을 확인하기 바란다.
In App Rage
어떤 앱을 만들어 볼까? 음, 우선 약간의 배경 설명을 하겠다.
최근 나는 rage comics로 불리는 온라인 유머게시판과 때때로 즐기는 “F7U12″에 빠지게 되었다. 이러한 것들을 들어본 적이 있는가? 요컨대 누군가 일반적인 실망스러운 상황에 처했을 때 거친 분노나 다른 유머 표현으로 마무리하는 재미있고 간단한 코믹이다.
그래서 본 튜토리얼에서는 사람들이 이들 코믹을 살 수 있도록 하는 “In App Rage”라는 간단한 앱을 만들게 될 것이다. 그러나 그 전에, 여러분들은 iOS Developer Center와 iTunes Connect를 이용하여 App ID 생성과 앱 엔트리 등록을 먼저 해야 한다.
먼저 앱을 위한 App ID를 생성하자. 그러기 위해서 iOS Developer Center로 접속한 다음 “App IDs”탭을 선택하고, “New App ID”를 클릭하자.
아레의 스크린샷 처럼 설명과 번들 식별자(bundle identifier)를 입력하자.
여러분 자신의 도메인 이름을 사용하여 bundle identifier를 입력해야 한다. 여러분의 이름이나 다른 어떤 것을 이용한 날조된 것은 사용할 수 없으며 정확히 유일한 ID여야 한다.
다 되었으면 Submit 버튼을 클릭하자. 이제 새로운 App ID를 가지게 되었다! 우리는 이것을 iTunes Connect에서 새로운 앱을 등록할 때 이용할 것이다 .
iTunes Connect로 접속한 후, “Manage Your Applications”을 클릭한 다음, “Add New App” 버튼을 클릭하자. iOS App과 Mac OSX App 선택화면이 나오면, 당연히 iOS App을 선택하자. 그리고 앱 이름, SKU number를 입력하고 위에서 생성해 두었던 Bundle ID를 선택하자.
앱 이름은 유일해야 하기 때문에 여러분은 아마도 앱 이름을 위의 그림에서 제시된 것이 아닌 다른 것으로 변경해야 할 것이다. 나는 이를 위해 내 이름의 이니셜 “RW”를 붙혔는데 여러분의 이름 이니셜로 변경하기 바란다.
다음 페이지에서 앱 정보들을 물어 올 것이다. 지금은 그냥 임시적인 간단한 정보만 입력하자. 나중에 변경할 수 있다. 그러나 불행히도 모든 항목을 채워넣어야 한다. 지금은 존재하지 않는 앱의 스크린샷 까지도…
여러분들이 아는 것 처럼, 나 또한 이런 상황이 맨붕이다.
위와 같은 에러가 발생하면 그냥 아무거나 입력하자. 그리고 여기 아이콘과 거짓 스크린샷으로 에러를 내뿜는 iTunes Connect를 달래 주자.
에러가 모두 해결되면 앱 등록이 완료된다. 오예!~
인앱구매 관리
인앱구매를 코드로 구현하기 전에 앱을 등록하였기 때문에 iTunes Connect에서 인앱구매 항목을 생성할 수 있다. 지금 막 등록한 앱에서 아래와 같이 “Manage In App Purchases”을 클릭하자.
그런 다음 좌측 상단의 “Create New”버튼을 클릭하자.
생성할 인앱구매의 형식을 선택하는 화면이 나타날 것이다. 대부분 자주 이용되는 형식은 두 가지이다.
- Consumables(소비형). 게임에서 흔히 볼 수 있는 생명력 보충, 게임머니, 임시강화제등과 같은 하나 이상을 구매하여 사용할 수 있는 것들이다.
- Non-Consumables(비소비형). 한 번의 구매로 계속적으로 이용할 수 있다. 추가 레벨이나 컨텐츠등이 이에 속한다.
우리가 만들고자 하는 In App Rage는 코믹이야기를 판매하는 것이므로 사용자는 한 번의 구매로 계속 이용할 수 있어야 한다. 따라서 Non-Consumable을 선택하겠다.
주의:모든 비소비형 구매는 사용자의 모든 기기에서 이용이 가능해야 한다. 예컨데 두 대의 기기를 가진 사용자가 있다면 동일한 인앱구매에 대해 두 번의 비용 결재가 되면 안된다는 것이다. 우리는 나중에 트랜잭션 복원에 대해 논의할 때 다른 디바이스에서 구매한 비소비형 컨텐츠를 사용자에게 제공하는 방법에 대해 다루게 될 것이다. 소비형의 경우에는 다르다. 소비형은 사용자가 구매한 기기에서만 사용될 수 있도록 하는 것이다. 디바이스들간의 소비형 구매 아이템 공유를 원한다면, 아이클라우드나 다른 기술을 이용해 직접 구현해야 한다.
다음으로, 인앱구매 항목에 대한 정보를 입력해야 한다. 아래의 그림을 참고하여 각 필드를 채워 보자.
각 필드에 대해 살펴보자.
- Reference Name: iTunes Connect에서 보여질 인앱구매 항목의 이름이다. 이것은 앱에서는 노출되지 않기 때문에 자유롭게 입력하면 된다.
- Product ID: 애플 문서에서는 “product identifier”로 표현되고 있는데, 인앱구매 항목을 식별할 수 있는 유일한 고유 문자열이다. 일반적으로 이 항목을 사용할 앱의 번들아이디로 시작하는 것이 가장 좋으며, 항목의 고유이름을 끝에 추가한다.
- Cleared for Sale: 앱이 앱스토어에 활성화 되자마자 구매가 가능한지 여부를 지정한다.
- Price Tier: 인앱구매 항목의 가격이다.
설정을 마친 후 언어(Language)섹션으로 스크롤 이동하여 Add Language 버튼을 클릭하고 아래 그림과 같이 입력하자.
이 정보들은 나중에 앱스토어에 인앱구매항목을 조회했을 때 받을 수 있는 정보들이다.
이 과정이 왜 필요한지 의아해 할지 모르겠다. 어쨌든 앱에 이 정보들을 포함할 수 있는데 말이다. 분명히 애플은 가격을 알 필요가 있다. 또한 앱스토어에서 이 정보의 일부가 표시된다. (최고 인앱구매항목을 표시하는것 처럼). 결국, 이러한 것들이 앱에 하드 코딩된 정보를 피하고, 구매를 활성화거나 비활성화하기 위한 목적으로 쉽게 적용될 수 있다.
모든걸 마쳤다면 저장하고 아래의 그림처럼 나머지 항목들을 생성하자. 항목의 설명에 대해서는 신경쓰지 말자. 본 튜토리얼에서는 사용되지 않는다. 항목의 Display Name만 이용될 것이다.
이들 과정이 시간을 꽤나 잡아 먹는다는걸 눈치챘을 것이다. 만일 1톤의 인앱구매 항목을 가지고 있다면 얼마나 성가신 일인지 상상이 간다. 다행히도 그런상황은 아니지만 만일 그렇다면 rage comic으로 가서 나를 그려라. :]
인앱구매 프로덕트 목록 가져오기
앱에서 사용자가 프로덕트에 대해 구매가 가능하도록 하기 전에 서버로부터 구매가능한 프로덕트 목록을 가져오는 쿼리를 iTunes Connect에 발행해야 한다.
이를 위해 프로덕트를 사용하는 뷰 컨트롤러에서 코드로 구현할 수 있겠지만, 재사용이 쉽지 않기 때문에 적절치 않다. 대신 인앱구매의 모든 동작을 관리하는 헬퍼 클래스를 만들 것이며, 여러분의 프로젝트에서도 쉽게 재사용할 수 있을 것이다.
서버로 부터 프로덕트 목록을 받는 것과 함께, 이 헬퍼 클래스는 구매완료된 것과 구매되지 않은 프로덕트를 기억할 것이다. 구매완료된 각 프로덕트 구분자는 NSUserDefaults로 저장할 것이다.
자, 시작하자. XCode에서 iOS Application의 Master-Detail Application 템플릿으로 프로젝트를 생성하자. 이름으로 InAppRage라고 입력하고, 디바이스는 iPhone으로 지정한 다음 Use Storyboards와 Use Automatic Reference Counting을 체크하자.
다음으로, 인앱구매에서 요구되는 StoreKit 라이브러리를 프로젝트에 추가하자. 이를 위해 프로젝트 네비게이터에서 프로젝트를 선택하고 In App Rage target을 선택하자. Build Phases 탭에서, Link Binary with Libraries 섹션을 확장하고, + 버튼을 클릭하자. 리스트에서 StoreKit.framework를 찾은 다음 Add 버튼을 클릭하자.
마지막 설정 단계로, Supporting Files의 In App Rage-Info.plist 파일을 열어서 Bundle identifier를 위에서 생성했던 App ID로 변경하자.
마침내 코딩을 할 시간이다. iOS Cocoa Touch의 Objective-C class 템플릿으로 파일을 생성하자. 클래스 이름은 IAPHelper로 하고 NSObject의 서브클래스가 되도록 하자.
IAPHelper.h 파일을 열어서 아래와 같이 변경하자.
이 클래스는 두 개의 메소드를 가지는데, 하나는 프로덕트 식별자(com.razeware.inapprage.nightlyrage와 같은) 목록을 넘겨서 초기화를 진행하고, 다른 하나는 iTunes Connect로 부터 프로덕트 정보를 가져오는 메소드이다. 두 번째 메소드는 비동기 방식으로 작동되며 정보 수신 완료시 호출자에게 통보할 수 있도록 블럭 파라미터를 넘긴다.
참고: 아직 블럭을 잘 모르겠는가? iOS 5 튜토리얼 시리즈에서 블럭을 사용하는 방법을 참고하자.
다음으로 IAPHelper.m 파일에 아래의 코드를 구현하자.
각 섹션별로 무엇을 했는지 살펴보자.
- In-App Purchase API를 사용하기 위해서는 StoreKit을 이용해야 하기 때문에 StoreKit 헤더를 포함한다.
- StoreKit으로부터 프로덕트 목록을 가져오려면 SKProductsRequestDelegate protocol을 구현해야 한다. 여기서 클래스 확장으로 이 프로토콜을 명시한다.
- 클래스가 유지되는 동안 프로덕트 목록을 가져오기 위해 발행하는 SKProductsRequest를 저장하는 인스턴스 변수를 선언한다. 클래스가 이미 활성화되었는지 판단하거나 클래스가 유지되는 동안 메모리에서의 보증을 요구하는 참조로 유지된다.
- 미해걸된 프로덕트 요청을 위한 completion handler, 전달 받은 프로덕트 식별자 목록 그리고 이전에 구매된 프로덕트 식별자 목록도 기억해야 한다.
다음으로 초기화 메소드를 추가하자.
여기서 NSUserDefaults에 저장된 값을 기초로 각 프로덕트들이 구매되었는지 여부를 판단하고 구매된 프로덕트 식별자를 리스트에 기억시킨다.
다음으로, iTunes Connect로 부터 프로덕트 정보를 가져오는 메소드를 구현하자.
먼저 프로덕트 비동기 요청이 완료되었을 때 호출자에게 통보할 수 있도록 completion handler의 copy를 인스턴스 변수에 저장한다.
그런 다음 iTunes Connect로 부터 정보를 가져오는 코드가 포함된, 애플이 작성한 클래스 SKProductsRequest 인스턴스를 생성하자. 사용은 아주 쉽다. SKProductsRequestDelegate 프로토콜을 따르는 델리게이트를 지정하고 실행을 위해 start 메소드를 호출하면 된다.
프로덕트 목록이 완료되거나 실패했을 때 productsRequest:didReceiveResponse 또는 request:didFailWithErorr 콜백을 받을 수 있도록 IAPHelper 클래스 자체를 델리게이트로 지정한다.
이제 델리게이트 콜백을 추가하자. 아래의 코드를 @end 이전에 추가하자.
성공과 실패를 위한 두 개의 델리게이트 콜백을 구현하였다. 성공일 때는 프로덕트 식별자, 지역화된 제목, 가격등과 같은 전송받은 프로덕트의 정보를 기록하고, 실패일 때는 _productsRequest 인스턴스 변수를 nil로 되돌리고 completion handler를 호출한다.
프로젝트를 빌드하여 컴파일에 오류가 없는지 확인하자.
앱을 위한 서브클래싱
IAPHelper 클래스는 프로덕트 식별자를 지정함으로써 여러분 자신의 앱을 위해 쉽게 서브클래싱할 수 있도록 작성되었다. 많은 사람들이 앱 업데이트보다 인앱구매 항목을 동적으로 추가할 수 있도록 프로덕트 식별자를 다른 정보와 함께 웹 서버로 부터 가져올 것을 추천한다.
당연하다. 하지만 본 튜토리얼에서는 여러분이 간단하게 진행하도록 하기 위해 프로덕트 식별자를 하드코딩하도록 하겠다.
iOS Cocoa Touch의 Objective-C class 템플릿으로 파일을 생성하고 클래스 이름은 RageIAPHelper로 하고IAPHelper의 서브클래스로 지정하자.
RageIAPHelper.h를 열어 아래와 같이 수정하자.
클래스의 싱글 글로벌 인스턴스를 리턴하는 static 메소드를 선언하였다.
다음으로 RageIAPHelper.m 을 열어 아래와 같이 수정하자.
sharedInstance 메소드는 RageIAPHelper 클래스의 싱글 글로벌 인스턴스를 리턴하는, Objective-C의 싱글톤 패턴으로 구현되었다. iTunes Connect에서 생성한 프로덕트 식별자들을 넘기는 것으로 슈퍼클래스의 초기화 메소드를 호출하였다.
이 프로덕트 식별자들은 iTunes Connect에서 생성한 실제 값이어야 한다는 것을 잊지 말자.
한 번 더 빌드해 보자. 컴파일에 에러가 없는지 다시 확인하자.
프로덕트 표시하기
프로덕트 정보들을 받았다. 이제 그것들을 스크린에 표시해 볼 시간이다.
MasterViewController.m 파일을 열어 아래의 코드로 모두 대치하자.
여기 훌륭한 약간의 코드가 있다. 하나씩 살펴 보자.
- iTunes Connect에서 받은 SKProduct 정보를 사용하기 위해 StoreKit 헤더와 함께 앞서 작성한 RageIAPHelper 클래스를 임포트하였다.
- iTunes Connect에서 받은 SKProduct를 저장하기 위한 인스턴스 변수를 추가하였다. 테이블뷰의 각 행에는 프로덕트 제목이 보여질 것이다.
- 이것은 굉장히 편리한 iOS 6의 새로운 잡아당겨 새로고침 테이블뷰 컨트롤의 예제이다. 위에서 보듯 굉장히 사용하기 쉽다. UIRefreshControl 인스턴스를 생성하고 UITableViewController의 멤버변수 refreshControl에 대입만 해주면 된다. 그런 다음 사용자가 새로고침을 위해 테이블을 잡아 당겼을 때 호출될 타겟을 등록해주면 된다. 여기서는 reload 메소드가 호출되도록 하였다. 기본적으로 이것은 테이블뷰가 처음으로 보여질 때 발생하지 않는다. 따라서 여기서는 처음으로 보여질 때 reload 메소드를 호출하고 리프레쉬 컨트롤에 첫 시작임을 코드로 직접 알리도록 하였다.
- 첫 시작, 또는 사용자가 새로고침을 위해 화면을 밑으로 당겨서 reload 메소드가 호출되었을 때, iTunes Connect로 부터 인앱구매 프로덕트 정보를 받기 위해 앞서 구현하였던 RageIAPHelper의 requestProductsWithCompletionHandler 메소드가 호출되도록 하였다. 이 요청이 완료되면 블럭이 호출될 것이다. 블럭안에서는 프로덕트 목록을 인스턴스 변수에 대입하고, 테이블뷰를 갱신하고, 리프레쉬 컨트롤의 애니메이션을 중지시키도록 하였다.
- SKProduct의 지역화된 제목을 각 행에 표시하기 위한 테이블뷰의 일반적인 구현이다.
빌드와 실행을 해 보자. 테이블뷰에 프로덕트 목록이 보여질 것이다.
잘 동작되지 않는가? 그렇다면 아래의 사항들을 확인해 보자. 다음의 내용은 포럼의 itsme.manish와 abgtan이 제공하였다.
- 기기의 “설정” -> “iTunes & App Stores”로 가서 모든 계정을 로그아웃하고 샌드박스 계정을 이용해 다시 시도해 보자.
- 여기를 확인해 보자. 응답이 없으면 iTunes sandbox가 다운되었을 수 있다.
- 해당 App ID의 인앱구매가 활성화되었는가?
- 해당 프로젝트 .plist의 Bundle ID가 App ID와 동일한가?
- SKProductRequest을 생성할 때 올바른 Product ID를 넘겼는가?
- iTunes Connect에 프로덕트를 추가한 이후 몇 시간정도 기다려 보고 다시 시도해 보자.
- iTunes Connect에 은행 계좌관련 정보를 정확히 입력하고 활성화 되었는가?
- 기기에서 앱을 삭제하고 재설치 해보자.
여전히 안되는가? 그러면 포럼 게시글이나 관련 코멘트를 살펴보자.
쇼미더머니
내용이 많이 길어졌지만 가장 중요한 부분이 아직 남아 있다. 구매를 구현하고 돈을 모으는 것!
구매 구현의 기본 요지는 아래와 같다.
- SKPayment 오브젝트를 만들고 사용자가 구매를 원하는 프로덕트 식별자를 전달해야 한다. 그리고 그것을 지불 큐에 추가해야 한다.
- StoreKit은 “정말로 구매하겠습니까?”라고 사용자에게 물어볼 것이고, 계정과 비밀번호 입력을 요구할 것이며, 요금청구를 생성하고, 성공 혹은 실패를 보낼 것이다. 또한 재다운로드를 위한 이미 구매된 것인 경우를 파악하여 그에 대한 메시지를 보낼 것이다.
- 여러분은 구매통보를 받기 위한 특별한 오브젝트를 지정해야 한다. 이 오브젝트는 컨텐츠 다운로드를 시작하고(여기서는 하드코딩되어 있기 때문에 필요가 없다.), 컨텐츠를 사용가능하도록 unlock할 것이다. (여기서는 NSUserDefaults와 purchasedProducts 배열에 구매정보를 저장할 것이다.)
걱정하지 말자. 코드를 보면 꽤 쉽다. 다시 한 번, 재사용을 쉽게 하기 위해 IAPHelper 클래스에서 대부분을 구현할 것이다. 아래의 코드를 IAPHelper.h 파일에 추가하자.
프로덕트가 구매되었을 때 통보를 받기 위한 통보식별자를 선언하였고, 프로덕트 구매를 시작하는 메소드와 구매가 완료되었는지를 판단하는 메소드를 선언하였다.
다음으로, IAPHelper.m 파일에 아래의 코드를 추가하자.
이 과정은 해당 프로덕트가 구매가능한 상태인지를 먼저 확인하고, 그렇다면 구배 진행중으로 표시한 다음 SKPaymentQueue에 SKPayment를 추가한다.
이것은 실제로 사용자에게 확실한 현금을 지불하도록 하는 것이다. 사용자들에게 거의 “닥치고 돈내놔!” 라고 말하는 것이나 다름 없다.
그러나, 사용자로 하여금 돈을 지불하도록 하려면 사용자에게 뭔가 좋은 것을 제공하여야 한다. 여러분들이 그리스 정부가 아닌 이상…
따라서, 여러분들은 지불 트랜잭션이 완료되었는지 판별하고 그것을 적절히 처리하는 코드를 추가해야 한다.
아주 쉽게 하도록 하자. 먼저 SKPaymentTransactionObserver를 구현하기 위해 IAPHelper 클래스의 확장을 아래와 같이 수정하자.
그런 다음 아래의 코드를 initWithProductIdentifiers 메소드에서 if 블럭 안에 추가하자.
이제, IAPHelper가 초기화 되었을 때, 자기 자신을 SKPaymentQueue의 transaction observer로 만들 것이다. 즉, 애플은 누군가 어떤 것을 구매했을 때 여러분에게 그것을 알리게 될 것이다.
여기서 정말로 중요한 것이 하나 있다. 사용자가 구매를 시작하고 (그리고 대금청구를 받고) 애플이 성공이나 실패 응답을 주기 전에 사용자가 앱을 종료하거나 네트워크 연결이 갑자기 끊어지는 경우가 종종 발생할 수 있다. 이때 사용자는 여전히 구매에 대한 기대감을 가지고 있겠으나 곧 엄청난 분노의 얼굴로 변할 것이다.
다행히도, 애플은 이에 대한 해결책을 가지고 있다. 그것은 앱에서 아직 완전히 처리되지 않은 모든 구매 트랜잭션을 애플이 기억하도록 하고 그것들을 transaction observer에 통보하는 것이다. 그러나 이것이 잘 동작되기 위해서는 앱이 초기화될 때 가능한 빨리 여러분의 클래스를 transaction observer로 등록하여야 한다.
이를 위해 AppDelegate.m 아래처럼 임포트하자.
그런 다음, 아래의 코드를 application:didFinishLaunchingWithOptions 메소드 시작 부분에 추가하자.
이제 앱이 실행되자마자 RageIAPHelper 싱글톤이 생성될 것이다. 이것은 앞서 수정한 initWithProducts메소드를 호출하도록 하여 그 자신을 transaction observer로 등록할 것이다. 그리하여 여러분은 완료되지 않은 모든 트랜잭션을 통보받을 수 있을 것이다.
여러분은 SKPaymentTransactionObserver 프로토콜을 구현해야 한다. IAPHelper.m에 아래의 메소드를 추가하자.
실제로 이것은 프로토콜에서 요구되는 메소드이다. 업데이트된 트랜잭션 목록을 여러분에게 제공하는데 여러분은 그 목록의 각 항목의 상태에 따라 달라진 부분을 처리해야 한다. 깔끔한 코드를 유지하기 위해 트랜잭션의 완료, 실패, 복원에 따라 각기 다른 메소드를 호출하도록 하였다.
완료와 실패는 알겠는데 복원은 무엇일까? 인앱구매 형식을 기억해 보자. 사용자가 비소비형 구매를 복원하는 방법이 있어야 한다고 앞서 설명했었다. 이것은 사용자가 같은 앱을 다른기기들에서 사용하거나 앱을 지웠다가 재설치하여 이전에 구매한 프로덕트를 사용하고자 할 경우 중요한 사안이 된다. 나중에 앱에 이 방법을 구현할 것이다. 지금은 사용자가 그들의 구매가 복원되기를 원할 때 해당 트랜잭션이 온다는 사실만 알도록 하자.
다음으로 중요한 completeTransaction, restoreTransaction, 그리고 failedTransaction 메소드를 구현하자. 아래의 코드를 추가하자.
completeTransaction과 restoreTransaction은 같은 작업을 수행한다. 주어진 컨텐츠를 제공하는 함수를 호출하는데 나중에 곧 구현할 것이다.
failedTransaction은 약간 다르다. 구매가 실패했음을 사용자에게 알리는 메소드를 호출하고(이것 역시 나중에 구현할 것이다) 더 이상 구매가 진행되지 않도록 하며 트랜잭션을 완료한다.
주의: finishTransaction 호출은 아주 중요하다. StoreKit이 처리가 완료되었읍을 알게 하는 것인데, 이를 호출하지 않으면 앱이 실행될 때마다 트랜잭션 전달이 계속될 것이다.
마침내, 아래의 마지막 코드를 추가하자.
프로덕트가 구매되었을 때, 이 메소드는 해당 프로덕트 식별자를 구매목록에 추가하고, NSUserDefaults에 이 정보를 저장한 다음 다른 곳에서 인식될 수 있도록 노티피케이션을 보낸다.
구매 코드는 끝났다. 이제 사용자 인터페이스에 적용하는 것만 남았다. 먼저 MainStoryboard.storyboard 열어서 table view cell의 스타일을 Subtitle로 변경하자.
그런 다음 MasterViewController.m 파일을 아래의 내용대로 수정하자.
여기서 통화형식으로 서브타이틀에 가격을 표시하였다. 그리고 구매되지 않은 프로덕트일 경우 액서서리뷰로 “buy” 버튼이 보이도록 하였고, 구매된 프로덕트일 경우 채크표시가 보이도록 하였다.
구매 버튼을 탭했을 때 호출될 메소드를 아래와 같이 구현하자.
여기서 tag를 이용한 간단한 방법으로 선택한 프로덕트를 알아내고, 구매를 위해 앞서 작성한 메소드를 호출하였다.
구매가 완료 되었을 때 노티피케이션을 보낸다는 사실을 기억하자. 그래서 노티피케이션을 등록하고 그것이 발생했을 때 해당 셀에 채크표시가 나타나도록 갱신하자.
자 이제 맛볼 시간이 거의 다 되었다. 그러나 테스트 계정을 먼저 생성해야 한다.
인앱구매와 계정 그리고 샌드박스
Xcode에서 앱을 실행하면 인앱구서버를 향하여 실행되는 것이 아니고 샌드박스 서버를 향하여 실행되는 것이다.
이것은 대금청구등과 같은 우려없이 구매를 할 수 있다는 것이다. 그러나 테스트 계정을 만들어야 하고 기기에서 앱스토어 로그아웃을 해야 한다. 이에 대한 전체과정을 살펴보자.
계정을 만들기 위해 iTunes Connect에 로그인하여 “Manage Users” 클릭, “Test User”를 클릭하고 들어가 샌드박스서버에서 임시로 인앱구매가 가능하도록 테스트 유저 계정을 만들자.
그런 다음 여러분의 아이폰에서 현재의 계정을 로그아웃하자. “설정”에서 “iTunes 및 App Store”로 들어가 현재 연결된 계정을 로그아웃하면 된다
자, 드디어, 앱을 실행하여 rage comic을 구매해 보자. 테스트 계정정보를 입력하고, 모든 진행이 잘 된다면 반가운 체크표시로 구매가 이루어질 것이다.
그런데, 잠깐, 코믹은 어딨지? 체크표시를 구매한 것도 아니고…
어쨋든, 이 포스팅이 너무 길어졌다. 코믹을 표시하는 작업은 인앱구매와는 별 관련이 없다. 그래서 추가연습으로 남겨두겠다.
resources zip 파일에 본 튜토리얼에 필요한 이미지와 코믹들이 포함되어 있으니 마음이 내키면 구매된 항목을 탭했을 때 새로운 뷰컨트롤러에서 코믹을 보여주도록 하기 바란다. 코믹을 보여주기 전에 InAppRageIAPHelper의 purchasedProducts 배열에 해당 프로덕트 식별자가 있는지 먼저 확인해야 할 것이다.
트랜잭션 복원
원 모어 씽!~
최근 인앱구매로 구현된 모든 앱에서 구매 트랜잭션을 복원할 수 있도록 하는 버튼이 요구된다. 앞서도 말했듯이, 사용자가 다른 기기에서 구매한 컨텐츠를 이용하고자 할 경우 매우 유용하다.
우리는 이미 견고한 프레임웍을 마련해 놓았기 때문에 믿을 수 없을 정도로 쉽게 구현할 수 있다.
먼저 IAPHelper.h 파일에서 아래의 메소드를 선언하자.
그리고 IAPHelper.m 파일에서 아래와 같이 구현하자.
오예!~ 이보다 더 간단할 수 없다. iTunes Connect로 연결하여 이미 구매한 비소비형 프로덕트를 발견해 내도록 하는 것이다. 그런 다음 각각에 대해 SKPaymentTransactionStateRestored로 paymentQueue:updatedTransactions를 호출할 것이다. 여러분은 여기서 이미 컨텐츠를 제공(unlock)하도록 구현하였다.
이제 남은 것은 이 메소드를 호출하는 것 뿐이다. MasterViewController.m에 아래의 코드를 추가하자.
이제 기기에서 앱을 삭제하고 재설치해 보자. 앱을 삭제함으로서 모든 NSUserDefaults가 제거되었기 때문에 체크표시에 대한 정보는 더 이상 존재하지 않는다.
Restore 버튼을 탭하고 몇 분 정도 기다리면 구매항목이 복원되어 체크표시로 다시 보일 것이다. 나쁘진 않다.
이제 어디로 갈까?
여기, 재사용가능한 인앱구매 헬퍼 클래스를 포함하여, 본 튜토리얼에서 우리가 개발한 코드들이 있는 프로젝트 소스가 있다.
위에서 제안했듯이 여러분이 내킨다면 코믹을 보여주는 기능을 추가하기 바란다.
인앱구매에 대해 좀 더 배우기를 원한다면, 우리의 새로운 책 iOS 6 튜토리얼를 참고하기 바란다. 아래의 내용을 다루는 2개의 챕터로 구성되어 있다.
=================================
=================================
=================================
실제 기기로 테스트하거나 앱스토어에 등록해 판매하지 않고 공부만 할 예정이라면, 개발환경구축 과정 필요 없이 시뮬레이터만으로도 가능하다. 이 때, 방법은 아래의 1, 2번 과정만 수행하면된다.
우선 아이맥과 아이팟터치를 최신 버전으로 업데이트 시켜줘야 합니다.
아이맥을 처음 실행시키고 인터넷에 연결되면 자동으로 업데이트가 시작됩니다. 또 아이팟터치도 아이튠즈와 싱크되면서 자동 업데이트 됩니다.
1. 애플 개발자 홈페이지 (http://developer.apple.com) 에서 회원가입을 합니다.
단, 이때 회원가입 정보와 차후 개발자 프로그램 구매 정보가 동일해야만 고생을 안합니다. 회원가입시 정확한 정보를 입력하시고, 다음 단계에서 개발자 프로그램 구매시에도 동일하게 정보를 입력해야 합니다.
2. 애플 아이폰 개발자 홈페이지 (http://developer.apple.com/iphone) 에서 로그인 후 SDK를 다운로드 받아 설치합니다.
애플 개발자 홈페이지는 크게 아이폰, 사파리, 맥OS로 나뉘어져 있습니다. 이 중 아이폰으로 이동하시면 됩니다.
단계 1에서 가입하신 정보로 로그인 하시면 최신 SDK를 다운로드 받을 수 있게 됩니다.
3. 애플 아이폰 개발자 프로그램 홈페이지 (http://developer.apple.com/iphone/program)에서 Apply하시면 됩니다.
개인사용자인지 기업사용자인지를 물어보는데 자신에 맞게 하시면 될 것 같습니다. 혹시나 1인 기업을 생각하고 계신다고 하더라도 개인사업자 자격이므로 개인사용자로 해도 문제가 없지 않을까 개인적으로 생각합니다.
지역도 물어보는데 한국을 선택하시면 됩니다.
등록을 완료하면 자동으로 한국 애플 스토어로 이동하고 장바구니에 자동으로 Standard Program이 들어 있는 것을 보실 수 있습니다. 가격은 부가세 포함하여 11만원이 조금 안됩니다. 이 것을 구매해야만 앱스토어에 등록할 수가 있습니다.
4. 하루정도가 지나면 회원가입시 작성한 이메일로 활성화 코드(iPhone Developer Activation Code)가 옵니다. 이메일 내의 코드 링크를 클릭하고 로그인을 하면 됩니다.
여기서 저는 인증에 문제가 생겼습니다. 그래서 다음 단계로 바로 진행이 안되고 신분을 확인하는 단계를 거쳐야 했습니다. 인증이 제대로 되었다면 "Program Portal"로 이동을 하거나 "애플 아이폰 개발자 홈페이지" 오른쪽에 "Program Portal"메뉴가 생성되었을 것입니다.
일단 받으신 인증코드 이메일은 절대 지우지 마시고 아래 방법으로 하시면 됩니다.
문제가 생기면 http://developer.apple.com/contact 로 가셔서 이메일을 보내시면 됩니다. 영어 실력이 개판인지라 아래처럼 딱 2줄 적어 보냈는데, 어이없게도 한글로 안내 이메일이 오더군요.
Im not activation,
What should I do authentication?
이메일 내용을 보니, 신분 확인이 안되니 신분증(여권, 주민등록증, 운전면허증 중) 사본을 보내는데, 그냥 보내서는 안되고 문서 공증을 받거나 경찰서에 가서 아무 경찰관에게나 신분 확인 받았다는 사인을 받고 보내라고 합니다.
어떤 분은 그냥 해줬다고도 하던데, 저는 조금 많이 잘못되었나 봅니다. ㅜ_ㅜ
신분증 복사해서 동네 지구대를 4번이나 갔는데 계속 순찰중이라 아무도 없더군요. 다섯번째야 사인을 받았는데, 도대체 뭐하는 것인지 궁금해 할까봐 애플로부터 받은 이메일까지 출력해서 갔습니다.
사본을 보낼때는 팩스로 보내는데 호주 전화번호더군요. 인터넷팩스를 이용했습니다.
그리고 보낼 때 "등록아이디 번호"도 신분증 사본에 적어 달라고 하는데요 이것은 "애플 아이폰 개발자 홈페이지"에 가서 로그인 후에 오른쪽 위의 "Edit Profile"을 클릭하셔서 들어가셔 보면 "Person ID"란에 10자리 숫자가 적혀 있을 겁니다.
이 숫자를 적으시면 됩니다.
애플 쪽에서 확인이 되면 다시 인증코드를 클릭해 보라는 이메일이 옵니다. 그러면 처음에 받았던 인증코드 이메일을 다시 열어서 클릭하시면 "Program Portal"로 이동합니다.
- See more at: http://k2man.net/821#sthash.TtRtBYLD.dpuf
=================================
=================================
=================================
출처: http://blueamor.tistory.com/1026
프로그래밍/iOS 2012/07/21 20:54
출처 : http://cafe.naver.com/mcbugi/219432
원문(리스토어와 인엡 풀소스) - http://www.changwoo.net/bbs/bbsDetail.do?&num=545
애플의 정책이 바뀌어 non-consume in-app은 무조건 restore(복구) UI가 있어야 합니다, 안그러면 리젝사유가 되어 가슴아픈 경험을 하게 됩니다.
그에 따른 로직을 올립니다.
버튼에 이벤트로
- (void) onClickRestore:(id)sender 를 호출하면
checkPurchasedItems ->
상황1 - 로그인 취소- > (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
상황2 - 로그인 성공 -> (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
요런 로직입니당
로그인이 성공하면
paymentQueueRestoreCompletedTransactionsFinished함수에서 그동안 구입한 인엡에 대한 목록(product id)을 얻어옵니다
그러면 콜백받은 인엡목록과 본어플의 인엡의 코드(product id)를 검사해서 있는지 확인한후 처리 하시면 되겠습니다.
//복구 버튼 이벤트
- (void) onClickRestore:(id)sender
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *languages = [defaults objectForKey:@"AppleLanguages"];
NSString *currentLanguage = [languages objectAtIndex:0];
NSString *requestString = @"";
//Language process
if ([currentLanguage isEqualToString:@"ko"]) {
// requestString = @"복구 요청중";
}else{
// requestString = @"Restore requesting";
}
HUD = [[MBProgressHUD alloc] initWithWindow:[[UIApplication sharedApplication] keyWindow]];
// HUD.center = CGPointMake(10, 110);
[[[UIApplication sharedApplication] keyWindow] addSubview:HUD];
HUD.delegate = self;
HUD.labelText = requestString;
[HUD show:YES];
[self checkPurchasedItems];
}
- (void) checkPurchasedItems
{
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
[HUD show:NO];
}// Call This Function
- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions
{
NSLog(@"- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions ");
}
// Sent when an error is encountered while adding transactions from the user's purchase history back to the queue.
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
NSLog(@"- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error ");
[HUD hide:YES];
}
// Then this is called
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSLog(@"%@",queue );
NSLog(@"Restored Transactions are once again in Queue for purchasing %@",[queue transactions]);
NSMutableArray *purchasedItemIDs = [[NSMutableArray alloc] init];
NSLog(@"received restored transactions: %i", queue.transactions.count);
//결재 기록이 없을때 alert 뛰우기
if(queue.transactions.count==0){
// NSString *fileMessage = NSLocalizedString(@"NOTRESTORE", @"restore");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *languages = [defaults objectForKey:@"AppleLanguages"];
NSString *currentLanguage = [languages objectAtIndex:0];
NSString *failMessage;
//Language process
if ([currentLanguage isEqualToString:@"ko"]) {
failMessage = @"구매 기록이 없습니다.";
}else{
failMessage = @"There is no record of your purchase.";
}
UIAlertView *resultView = [[UIAlertView alloc] initWithTitle:@"Failed"
message:failMessage
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil];
[resultView show];
}
for (SKPaymentTransaction *transaction in queue.transactions)
{
NSString *productID = transaction.payment.productIdentifier;
[purchasedItemIDs addObject:productID];
NSLog (@"product id is %@" , productID);
// here put an if/then statement to write files based on previously purchased items
// example if ([productID isequaltostring: @"youruniqueproductidentifier]){write files} else { nslog sorry}
if([productID isEqualToString:KONGLISH_PRODUCTID])
{
//재구입확인dh
NSLog(@"already buy");
}
}
[HUD hide:YES];
}
=================================
=================================
=================================
출처: http://www.changwoo.net/bbs/bbsDetail.do?&num=545
in app restore
작성자 : 이창우 (x1wins) | 등록일 : 2012-06-21 | 목록
첨부 파일이 없습니다.
첨부 이미지가 없습니다.
//restore button { /* *구매확인버튼 iPad- 915, 14 4G- 854, 15 */ int x; int y; if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { x = 915; y = 14; }else { x = 854/2; y = 15/2; } UIImage *img = [UIImage imageNamed:deviceFile(@"btn_Confirmation", @"png")]; UIImage *imgOver = [UIImage imageNamed:deviceFile(@"btn_Confirmation_o", @"png")]; restoreButton = [UIButton buttonWithType:UIButtonTypeCustom]; restoreButton.frame = CGRectMake(x, y, img.size.width, img.size.height); [restoreButton addTarget:self action:@selector(onClickRestore:) forControlEvents:UIControlEventTouchUpInside]; [restoreButton setImage:img forState:UIControlStateNormal]; [restoreButton setImage:imgOver forState:UIControlStateHighlighted]; [self.view addSubview:restoreButton]; } |
viewDidLoad
#pragma mark -- buy or nobuy - (void) goBuy { //구입했을때 } - (void) goNoBuy { //구입안ㅤㅎㅒㅆ을때 } #pragma mark - #pragma mark MBProgressHUDDelegate methods - (void)hudWasHidden:(MBProgressHUD *)hud { // Remove HUD from screen when the HUD was hidded [HUD removeFromSuperview]; } #pragma mark Payment methods (StoreKit-IAP) - (void) onClickRestore:(id)sender { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSArray *languages = [defaults objectForKey:@"AppleLanguages"]; NSString *currentLanguage = [languages objectAtIndex:0]; NSString *requestString = @""; //Language process if ([currentLanguage isEqualToString:@"ko"]) { // requestString = @"복구 요청중"; }else{ // requestString = @"Restore requesting"; } HUD = [[MBProgressHUD alloc] initWithWindow:[[UIApplication sharedApplication] keyWindow]]; // HUD.center = CGPointMake(10, 110); [[[UIApplication sharedApplication] keyWindow] addSubview:HUD]; HUD.delegate = self; HUD.labelText = requestString; [HUD show:YES]; [self checkPurchasedItems]; } - (void) checkPurchasedItems { [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; [HUD show:NO]; }// Call This Function - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { NSLog(@"- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions "); } // Sent when an error is encountered while adding transactions from the user's purchase history back to the queue. - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { NSLog(@"- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error "); [HUD hide:YES]; } // Then this is called - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { NSLog(@"%@",queue ); NSLog(@"Restored Transactions are once again in Queue for purchasing %@",[queue transactions]); NSMutableArray *purchasedItemIDs = [[NSMutableArray alloc] init]; NSLog(@"received restored transactions: %i", queue.transactions.count); //결재 기록이 없을때 alert 뛰우기 if(queue.transactions.count==0){ // NSString *fileMessage = NSLocalizedString(@"NOTRESTORE", @"restore"); NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSArray *languages = [defaults objectForKey:@"AppleLanguages"]; NSString *currentLanguage = [languages objectAtIndex:0]; NSString *failMessage; //Language process if ([currentLanguage isEqualToString:@"ko"]) { failMessage = @"구매 기록이 없습니다."; }else{ failMessage = @"There is no record of your purchase."; } UIAlertView *resultView = [[UIAlertView alloc] initWithTitle:@"Failed" message:failMessage delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [resultView show]; } for (SKPaymentTransaction *transaction in queue.transactions) { NSString *productID = transaction.payment.productIdentifier; [purchasedItemIDs addObject:productID]; NSLog (@"product id is %@" , productID); // here put an if/then statement to write files based on previously purchased items // example if ([productID isequaltostring: @"youruniqueproductidentifier]){write files} else { nslog sorry} if([productID isEqualToString:KONGLISH_PRODUCTID]) { //재구입확인dh NSLog(@"already buy"); } } [HUD hide:YES]; } - (void) onClickBuy:(id)sender { HUD = [[MBProgressHUD alloc] initWithWindow:[[UIApplication sharedApplication] keyWindow]]; // HUD.center = CGPointMake(10, 110); [[[UIApplication sharedApplication] keyWindow] addSubview:HUD]; HUD.delegate = self; HUD.labelText = @"결제 요청중"; [HUD show:YES]; // [HUD showWhileExecuting:@selector(startPay) onTarget:self withObject:nil animated:YES]; if ([SKPaymentQueue canMakePayments]) { // 스토어가 사용 가능하다면 NSLog(@"Start Shop!"); [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; // Observer를 등록한다. SKPayment *payment = [SKPayment paymentWithProductIdentifier:KONGLISH_PRODUCTID]; [[SKPaymentQueue defaultQueue] addPayment:payment]; // SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:KONGLISH_PRODUCTID]]; // productRequest.delegate = self; // [productRequest start]; } else { NSLog(@"Failed Shop!"); UIAlertView *resultView = [[UIAlertView alloc] initWithTitle:@"Failed" message:@"Sorry. Try again!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [resultView show]; } } - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; [self goBuy]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; [self goBuy]; break; default: break; } } } - (void) restoreTransaction: (SKPaymentTransaction *)transaction { [HUD hide:YES]; NSLog(@"SKPaymentTransactionStateRestored"); [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; UIAlertView *resultView = [[UIAlertView alloc] initWithTitle:@"결재 완료" message:@"이미 구매 해 주셨었습니다. 감사합니다." delegate:self cancelButtonTitle:nil otherButtonTitles:@"확인", nil]; [resultView show]; } - (void) failedTransaction: (SKPaymentTransaction *)transaction { // [HUD show:NO]; [HUD hide:YES]; NSLog(@"SKPaymentTransactionStateFailed"); [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; UIAlertView *resultView = [[UIAlertView alloc] initWithTitle:@"Failed" message:@"Sorry. Try again!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [resultView show]; } - (void) completeTransaction: (SKPaymentTransaction *)transaction { [HUD hide:YES]; NSLog(@"SKPaymentTransactionStatePurchased"); NSLog(@"Trasaction Identifier : %@", transaction.transactionIdentifier); NSLog(@"Trasaction Date : %@", transaction.transactionDate); [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; UIAlertView *resultView = [[UIAlertView alloc] initWithTitle:@"결재 완료" message:@"구매 해주셔서 감사합니다." delegate:self cancelButtonTitle:nil otherButtonTitles:@"확인", nil]; [resultView show]; } - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { HUD.labelText = @"결제 처리중"; NSLog(@"SKProductRequest got response"); if( [response.products count] > 0 ) { SKProduct *product = [response.products objectAtIndex:0]; NSLog(@"Title : %@", product.localizedTitle); NSLog(@"Description : %@", product.localizedDescription); NSLog(@"Price : %@", product.price); } if( [response.invalidProductIdentifiers count] > 0 ) { NSString *invalidString = [response.invalidProductIdentifiers objectAtIndex:0]; NSLog(@"Invalid Identifiers : %@", invalidString); } } |
new in app policy
=================================
=================================
=================================
출처: http://devpark.egloos.com/viewer/11917
4-2. 내 프로젝트에서 native extension code 이용하기
- flash builder에서 flex mobile project로 project를 하나 만든다 (만들때 첫화면에 default로 위에서 넣어준 최신버전 선택하고있는지 확인).
- 아래화면처럼 셋팅하고 finish.
- 만들어진 프로젝트에서 마우스 오른쪽 버튼을 눌러 properties-> Flex Build Path 선택
- 선택되있는 Library path탭에서 add SWC를 클릭해서 svn으로 받은 폴더안에 있는 .swc파일을 선택한다.
- 아래 화면같이 나오는데 merge into code를 아래화면처럼 external로 바꿔준다.
- 바로 옆 탭인 "Native Extensions" -> add ANE 클릭. 역시 압축푼 폴더 안에있는 appPurchase.ane를 선택.
- 같은 properties 화면에서 이번엔 Flex Build Path바로위에있는 Flex Build Packaging->Apple iOS선택
- 다음으로 native extensions탭을 눌러 아까 추가해준 ane에 체크를 해주고 ok를 눌러 properties를 종료
- project명-app.xml 파일을 클릭하고 <extension>..</extenstion>을 아래와 같이 </initalwindow>밑에 추가해주고 저장....<extensions></extensions>
- <extensionID>com.adobe.appPurchase</extensionID>
- </initialWindow>
- *아래 내용을 추가하기 전에 스크롤을 맨아래로 내려서 비슷한 내용이 이미 추가되어있다면 스킵
=================================
=================================
=================================
기타 참고 사이트:
https://www.raywenderlich.com/36270/in-app-purchases-non-renewing-subscription-tutorial
=================================
=================================
=================================
'스마트기기개발관련 > IOS 개발' 카테고리의 다른 글
ios 파일 이동 삭제 생성 관련 (0) | 2020.09.22 |
---|---|
adobe air as3 iPhone 4용 듀얼 브라우저 앱 만들기 (0) | 2020.09.22 |
ios error itms-9000 no .app bundles found in the package (0) | 2020.09.22 |
air 4.0 의 ios 기기의 swf 다운로드 후 로드 관련 (0) | 2020.09.22 |
ios 개발 air 하이브리드 앱 개발 할때 air로 swf 불러들이기(액션스크립트까지 호환되도록) (0) | 2020.09.21 |