iOS in-app purchase

iOS 2019. 4. 14. 20:17

iOS 결제 모듈을 사용하려면, Provisioning Profile이 있어야 합니다.

 

선행작업.

프로비저닝 프로파일 및 푸쉬 인증서 생성 : https://nicgoon.tistory.com/202 .

 

공식 문서.

StoreKit : https://developer.apple.com/documentation/storekit .
In-App Purchase : https://developer.apple.com/documentation/storekit/in-app_purchase .

 

참고 문서.

참고 1 : https://medium.com/@bestiosdevelope/implement-in-app-purchase-iap-in-ios-applications-swift-4d1649509599 .
참고 2 : https://devmjun.github.io/archive/purchase-1 . (자세히 설명되어 있음).
참고 영상 2 : https://www.youtube.com/watch?v=o3hJ0rY1NNw .

 

참고 사이트.

Itunes Connect : https://itunesconnect.apple.com/ .
Apple ID : https://appleid.apple.com/

 

1. 테스터 등록.

테스터로 사용할 계정을 위해 메일을 만들고 애플 아이디에 접속해 계정을 생성할 필요가 없습니다. 애플 아이디에 메일을 그냥 입력하면 되며, 심지어는 실제 사용되고 있는 애플 아이디를 넣는 다면, 이미 사용중 이라는 오류가 발생합니다.

먼저 아이툰즈 컨넥트에 접속합니다. 사용자 및 액세스를 클릭 합니다.

사용자 및 액세스 화면이 나오면, Sandbox > 테스터를 선택합니다. 그리고 나온 화면에 + 키를 눌러 주도록 합니다.

그럼 아래와 같이 테스터를 초대 하는 화면이 나올 텐데, 적당히 입력하고 초대 버튼을 눌러 줍니다. 주의 할 점은 실제 사용중인 애플 아이디를 넣으면 오류가 납니다. 자신이 사용하지 않고, 다른 사람도 사용중이 않을 메일 계정을 넣어 테스터 계정으로 사용하도록 합니다.

2. 앱 등록.

테스트 앱을 등록해 주도록 합니다. 저는 번들 아이디가  com.tistory.yeslife.inapp 인 inapp 이라는 앱을 등록할 것 입니다.

먼저 애플 스토어 컨넥트 에서 나의 앱을 선택합니다.

나의 앱 페이지가 나타나면, + 를 눌러 앱을 추가하도록 합니다.

신규앱을 등록하는 폼이 나타나면 아래와 같이 입력해 줍니다. 이름은 앱스토어 내에서 고유해야 합니다. 이미 사용중인 경우, 오류가 납니다. SKU는 앱의 상점을 위한 아이디인데, 적당히 고유한 값을 주면됩니다. 다 입력했으면 생성 버튼을 눌러 주도록 합니다. 

앱 이 생성되고, 앱이 표시 되면, 메뉴의 [앱 내 추가 기능]을 선택해 주도록 합니다.

그럼 앱내 구입 항목을 선택할 수 있는데 여기서 + 버튼 눌러 소모성 상품을 추가하도록 합니다.

앱 내 생성하려는 구입 선택항 목에서, 소모품을 선택하고 생성 버튼을 눌러 주도록 합니다.

그 다음 상품을 넣는 창에서 각각 적당한 값을 넣으면 되지만, 제품 ID는 각 상품마다 고유한 값을 잘 넣어 줘야 합니다.

그럼 등록되었지만, [메타 데이터 누락됨] 이라고 나올 수 있는데, 이 것은 우리가 심사를 위한 데이터를 넣지 않아 생기는 일이고, 테스트를 위한 것은 여기까지 입력하면됩니다.

저는 아래와 같이 3개의 소모성 아이템을 구매 했습니다.

 

앱은 현재 메타 데이터가 누락되어 있는데, 메타데이터를 설정된 뒤, 다음과 같이 앱내 구입으로 선택되어야 합니다.

 

3. 결제 옵저버.

swift에서 옵저버는 멤버 프로퍼티의 값이 변할 때, 알려주는 일종의 콜백 입니다. 결제가 시작될 때, 거절 될때등, 결제 상태를 확인해 이를 처리할 옵저버가 필요합니다.

옵저버는 SKPaymentTransactionObserver 프로토콜(c#의 인터페이스)을 상속해 구현해야 합니다. 코드는 대략 아래와 같습니다.


class StoreObserver: NSObject, SKPaymentTransactionObserver {
    
    
    // 멤버 변수들.
    var purchased = [SKPaymentTransaction]();
    
    
    
    
    override init() {
        super.init()
        
        // 생성자를 위한 초기화 메소드.
        
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        
        
        
        for transaction in transactions
        {
            
            switch transaction.transactionState
            {
                
            // 결제가 진행 중인 경우.
            case .purchasing :
                print("결제가 진행되고 있습니다.");
                break;
                
            // 결제 창을 띄우는 데 실패했습니다.
            case .deferred:
                print("아이폰의 잠기는 등의 이유로 결제 창을 띄우지 못했습니다.");
                SKPaymentQueue.default().finishTransaction( transaction );
                break;
                
            // 결제를 성공한 경우.
            case .purchased:
                print("결제를 성공하였습니다.");
                handlePurchased( transaction );
                SKPaymentQueue.default().finishTransaction( transaction );
                break;
                
            // 결제를 실패한 경우.
            case .failed:
                print("결제를 실패하였습니다.");
                SKPaymentQueue.default().finishTransaction( transaction );
                break;
                
            // 결제 검증을 하였습니다.
            case .restored:
                print("상품 검증을 하였습니다.");
                SKPaymentQueue.default().finishTransaction( transaction );
                break;
                
            default :
                print("알수 없는 오류를 만났습니다.")
                SKPaymentQueue.default().finishTransaction( transaction );
                break;
                
            }
            
        }
        
        
        
    }
    
    
    func handlePurchased( _ transaction : SKPaymentTransaction ) {
        
        purchased.append(transaction)


        SKPaymentQueue.default().restoreCompletedTransactions();

        
        print( "영수증 주소 : \(Bundle.main.appStoreReceiptURL)" );
        
        
        let receiptData = NSData( contentsOf: Bundle.main.appStoreReceiptURL! );
        print(receiptData)
        
        let receiptString = receiptData!.base64EncodedString(options: NSData.Base64EncodingOptions());
        
        
        print ( "구매 성공 트랜젝션 아이디 : \(transaction.transactionIdentifier!)" );
        print ( "상품 아이디 : \(transaction.payment.productIdentifier)" );
        print ( "구매 영수증 : \(receiptString)" );
     
        // 결제를 마무리 하도록 합니다.
        SKPaymentQueue.default().finishTransaction( transaction );


        
    }
    
    
}

대게의 코드는 공식 문서를 통해 쉽게 알 수 있지만 중요한 부분은 영수증을 받는 부분 입니다. 이 부분은 크게 언급이 공식 문서에 없어, 애를 먹었는데, Bundle.main.appStoreReceiptURL 값에 영수증 파일명이 들어 있습니다. 이것을 NSData를 이용해 값을 가지고 오고, Base64  인코딩을 해 사용하면됩니다.

결제를 할 때마다 transaction 이 생성되는 데, 실패하던, 걸절하던, 꼭 finishTransaction 처리를 해 줘야 합니다. 그렇지 않으면, 껐다 키면 다시 트랜젝션값들이 계속 추가 됩니다. 이 것은 앱이 비정상적으로 종료되었을 때도, 처리를 하기 위함으로 보입니다.

 

4. 상품 리스트 가지고 오기 및 구매 처리.

결제는 대체로 뷰에서 일어 나므로, viewController에서 처리하는 것이 좋습니다.

처음 상품 리스트를 가지고 오는 부분이 있습니다. 이때, 위에서 등록한 상품의 ID들을 적어 주면됩니다. 코드는 아래와 같습니다.

import UIKit
import StoreKit

class ViewController: UIViewController, SKProductsRequestDelegate {
    
    
    // 멤버 변수들.
    let iapObserver = StoreObserver()
    var productRequest : SKProductsRequest?
    
    // 입력 받은 상풉 정보들을 저장하는 변수들.
    var validProductArray = [SKProduct]();
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // 페이먼트 관련 설정을 해 주도록 합니다.
        SKPaymentQueue.default().add(iapObserver)
        
        // 상품 정보를 가지고 오는 리퀘스트를 설정하도록 합니다.
        let pIDs = Set(["gold500","gold100"]);
        productRequest = SKProductsRequest( productIdentifiers: pIDs );
        productRequest!.delegate = self;
        
        // 앱스토어 상품 정보를 요청합니다.
        productRequest!.start();
        
        
    }

    
    // 구매 진행 가능 여부를 확인합니다.
    @IBAction func checkPaymentAble(_ sender: Any) {
        
        if( SKPaymentQueue.canMakePayments() )
        {
            print("결제 요청이 가능합니다.");
        }else
        {
            print("결제 요청이 불가능합니다.");
        }
        
    }
    
    @IBAction func tryPaymentQueue(_ sender: Any) {
        
        print( "골드 100개 결제 할거에요." )
        
        let payment = SKMutablePayment( product: validProductArray[0] );
        
        SKPaymentQueue.default().add(payment)
        
        print( "변화가 있나요?" )
        
    }
    
    
    // 상품 정보를 받은 후 처리 메소드 입니다.
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        
        
        print("상품갯수 \(response.products.count)")
        print("확인하지 못한 상품 갯수 \(response.invalidProductIdentifiers.count)")
        
        
        for product in response.products
        {
            print("이름:\(product.localizedTitle)\n가격:\(product.price)")
            validProductArray.append(product)
        }
        
        
        
        
        
    }
    
    
}

 

이 것만으로 결제는 코드는 끝 입니다. 실제 결제를 해 볼때는 아이폰에서, 애플 아이디를 로그아웃하는 것이 좋습니다.

 

5. 검증 서버. (php)

원래 node js만 구현하려고 했으나, 잘 되지 않아 php로 먼저 결제 서버를 구현하였고, 그것을 바탕으로 node.js를 구현하였습니다.

애플 결제는 서버로 검증하는 것이 100배 쉽습니다. 그냥 POST 로 영수증을 Json형태로 보내면 끝 입니다. 받은 값에, 결제 날자 구매 수량 등이 있으므로, 이 것을 비교해 앱의 정보를 확인하면 되겠습니다. 아래는 샌드박스 주소로 되어 있습니다. 실제 결제 주소는 https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW2 에서 확인할 수 있습니다.

                // 영수증.
                $receipt = "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";
                $addr = "https://sandbox.itunes.apple.com/verifyReceipt";
                $host = "sandbox.itunes.apple.com";
                $path = "/verifyReceipt";

                // 보낼 json 데이터를 만들어 주도록 합니다.
                $bodyJson = '{ "receipt-data" : "' . $receipt . '" }';


                // 요청을 위한 주소를 만들어 주도록 합니다.
                $endline = "\r\n";
                $req = "";


                // 주소 및 타입 관련 헤더를 추가합니다.
                $req = "POST {$path} HTTP/1.1" . $endline
                . "Host: {$host}" . $endline
                . "Content-Type: application/json" . $endline
                ;

                // 데이터를 보내기 위한 기본 헤더를 추가합니다.
                $req .= "Content-Length: " . strlen($bodyJson) . $endline
                . "Connection: Close" . $endline
                ;

                // 헤더의 끝을 표시하는 빈 문자열을 설정합니다.
                $req .= $endline;
                
                // 보낼 내용을 추가합니다.
                $req .= $bodyJson;
                ;




                // 영수증 서버에 접속을 시도합니다.
                $fsock = @fsockopen( "ssl://{$host}", 443 );

                // 영수증 서버에 접속을 실패한 경우 처리 입니다.
                if( !$fsock )
                {
                    echo "구글 서버에 접속 실패하였습니다.!";
                    exit;
                }
                
                // 데이터를 보내도록 합니다.
                fwrite( $fsock, $req );


                // 데이터 받기를 위해 필요한 값들을 선언합니다.
                $headPassed = false;
                $resDatas = "";


                // 데이터 받기가 완료될 때 까지 대기하면 데이터를 받아 출력합니다.
                while( !feof($fsock) )
                {

                    // 한 줄 라인을 가지고 옵니다.
                    $line = fgets($fsock, 128);

                    // 아직 헤더는 아니지만, 헤더의 끝을 만난 경우, 헤더가 끝났음을 마킹하고, 종료합니다.
                    if( $line == "\r\n" && !$headPassed )
                    {
                        $headPassed = true;
                        continue;
                    }

                    // 헤더가 아닌 경우만, 값을 출력하도록 합니다.
                    if( $headPassed )
                    {
                        $resDatas .= $line;
                    }

                }

                // 연결을 닫아 주도록 합니다.
                fclose( $fsock );



                // JSON 데이터를 표시하도록 합니다.
                echo "받은 데이터 : " . $resDatas;


                echo "보낸 데이터 :" . $req;

 

6. 검증서버 (node.js)

알고리즘 및 구현은 php와 동일하므로, 설명을 생략하겠습니다.

const request_json = require("request-json");



// 인앱 서버 주소.
const serverAddr = "https://sandbox.itunes.apple.com/verifyReceipt";
// const serverAddr = "https://buy.itunes.apple.com/verifyReceipt";

// 구매 영수증 주소.
const receipt = "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";



const host = "https://sandbox.itunes.apple.com";
const path = "/verifyReceipt";



const receiptJson = {
    'receipt-data': receipt
};



var client = request_json.createClient( host );
client.post( path, receiptJson, function( err, res, body ){

    if( err )
    {
        console.error( err );
        return;
    }

    console.log( JSON.stringify( body ) );

});

 

7. Restore

앱을 지우거나 했을 때, 기존의 결제 내역을 복구하는 기능입니다. 저 같은 경우 서버에서, 결제 검증을 하고, 데이터를 가지고 있을 것 이므로, 필요하지 않아 구현하지 않았습니다.

 

 

 

Posted by 창업닉군
,