오르다 보면 언젠가는 정상에 다다르게 된다.

iOS

Apple App Store Server Notifications Version 1 with PHP Codeigniter, App Store 서버 알림

Looplian 2023. 5. 17. 12:52
반응형

https://developer.apple.com/kr/help/app-store-connect/configure-in-app-purchase-settings/enter-server-urls-for-app-store-server-notifications

 

App Store 서버 알림에 대한 서버 URL 입력 - 앱 내 구입 설정 진행 - App Store Connect - 도움말 - Apple Devel

앱 내 구입 설정 진행 App Store 서버 알림에 대한 서버 URL 입력 “App Store 서버 알림”은 구독 상태 변경 또는 앱 내 구입의 환불과 같이 앱 내 구입과 관련된 주요 이벤트의 정보를 제공합니다. App

developer.apple.com

 

애플 앱스토어에 앱등록을 진행하다보면 App Store 서버 알림이라는 부분을 등록해야 할때가 있다

이는 애플 서버에서 사용자의 구독 , 구독 취소, 환불 요청 시작, 환불 등 중요한 정보에 대한 알림을 받을 서버 주소를 입력해 주어야 한다.

위 의 페이지에서 변경 할 수 있고 변경을 누를 경우 버전 1과 버전2중 선택을 하여 등록할수 있다.

지금은 버전1에 대한 설명을 하려고 한다.

서버에서 알림은 JSON 형태로 오게 된다 그래서 file_get_contents( 'php://input' )으로 데이터를 받아 가공 처리하면된다.

Codeigniter 에서는 $this->input->raw_input_stream 으로 받아 처리하면된다.

 

function callback()
{
    $callBackData = json_decode($this->input->raw_input_stream, true);
    if ($callBackData['notification_type'] == 'CANCEL')
    {
        // Apple 지원이 자동 갱신 구독을 취소했고 고객이 의 타임스탬프를 기준으로 환불을 받았음을 나타냅니다
    }
    else if ($callBackData['notification_type'] == 'DID_CHANGE_RENEWAL_PREF')
    {
        // 고객이 다음 갱신 시 적용되는 구독 요금제를 변경했음을 나타냅니다. 현재 활성 계획은 영향을 받지 않습니다. 고객의 구독이 갱신되는 제품의 제품 식별자를 검색하려면 필드를 확인하십시오 .auto_renew_product_idunified_receipt.Pending_renewal_info
    }
    else if ($callBackData['notification_type'] == 'DID_CHANGE_RENEWAL_STATUS')
    {
        // 구독 갱신 상태의 변경을 나타냅니다. JSON 응답에서 마지막 상태 업데이트 날짜 및 시간을 검색하도록 확인합니다. 현재 갱신 상태를 확인 하려면 선택하십시오.auto_renew_status_change_date_msauto_renew_status
    }
    else if ($callBackData['notification_type'] == 'DID_FAIL_TO_RENEW')
    {
        // 청구 문제로 인해 갱신에 실패한 구독을 나타냅니다. 구독의 현재 재시도 상태를 검색하려면 선택하십시오 . 구독이 청구 유예 기간인 경우 새 서비스 만료 날짜를 확인 하려면 선택하십시오.is_in_billing_retry_periodgrace_period_expires_date
    }
    else if ($callBackData['notification_type'] == 'DID_RECOVER')
    {
        // 과거에 갱신에 실패한 만료된 구독의 성공적인 자동 갱신을 나타냅니다. 다음 갱신 날짜 및 시간을 확인하려면 확인하십시오 .expires_date
    }
    else if ($callBackData['notification_type'] == 'DID_RENEW')
    {
        // 고객의 구독이 새로운 거래 기간 동안 성공적으로 자동 갱신되었음을 나타냅니다. 고객에게 구독 콘텐츠 또는 서비스에 대한 액세스 권한을 제공합니다.
    }
    else if ($callBackData['notification_type'] == 'INITIAL_BUY')
    {
        // 사용자가 구독을 처음 구매할 때 발생합니다. App Store에서 유효성을 검사하여 언제든지 사용자의 구독 상태를 확인할 수 있도록 서버에 토큰으로 저장하십시오 .latest_receipt
    }
    else if ($callBackData['notification_type'] == 'INTERACTIVE_RENEWAL')
    {
        // 고객이 앱의 인터페이스를 사용하거나 계정의 구독 설정에 있는 App Store에서 대화식으로 구독을 갱신했음을 나타냅니다. 즉시 서비스를 제공하십시오.
    }
    else if ($callBackData['notification_type'] == 'PRICE_INCREASE_CONSENT')
    {
        // App Store에서 동의가 필요한 앱의 자동 갱신 구독 가격 인상에 대한 동의를 고객에게 요청하기 시작했음을 나타냅니다.
        // 개체 에서 값은 사용자가 아직 가격 인상에 응답하지 않았음을 나타냅니다.unified_receipt.Pending_renewal_infoprice_consent_status0
        // App Store 서버는 고객이 가격 인상에 동의하는 시점 을 로 설정합니다.price_consent_status1
        // App Store Server API에서 Get All Subscription Statuses 엔드포인트를 호출하여 최신 가격 동의 상태를 확인하십시오 . 에서 필드를 확인하십시오 . 또한 verifyReceipt를 호출하여 업데이트된 가격 동의 상태를 볼 수 있습니다.priceIncreaseStatusJWSRenewalInfoDecodedPayload
        // 고객 동의가 필요한 구독 가격 인상에 대한 가격 동의 시트를 표시하기 전에 StoreKit이 앱을 호출하는 방법에 대한 자세한 내용은 을 참조하십시오 . 구독 가격 관리에 대한 자세한 내용은 가격 관리를 참조하세요.paymentQueueShouldShowPriceConsent(_:)
    }
    else if ($callBackData['notification_type'] == 'REVOKE')
    {
        // 가족 공유를 통해 사용자에게 부여된 인앱 구매를 공유를 통해 더 이상 사용할 수 없음을 나타냅니다. StoreKit은 구매자가 제품에 대해 가족 공유를 비활성화했거나 구매자(또는 가족 구성원)가 가족 그룹을 떠났거나 구매자가 환불을 요청하고 받았을 때 이 알림을 보냅니다. 앱에서도 전화를 받습니다 . 가족 공유에 대한 자세한 내용은 앱에서 가족 공유 지원을 참조하십시오 .paymentQueue(_:didRevokeEntitlementsForProductIdentifiers:)
    }
    else if ($callBackData['notification_type'] == 'CONSUMPTION_REQUEST')
    {
        // 고객이 소모성 인앱 구매에 대한 환불 요청을 시작했으며 App Store에서 소비 데이터 제공을 요청하고 있음을 나타냅니다. 자세한 내용은 소비 정보 보내기를 참조하십시오 .
        // transaction_id 에 해당하는 사용자의 아이디를 찾아 환불 사전정보를 전달해준다. 
        $this->sendAppleConsumption($callBackData['original_transaction_id']);
    }
    else if ($callBackData['notification_type'] == 'REFUND')
    {
        // App Store에서 소모성 인앱 구매, 비소모성 인앱 구매 또는 비갱신 구독에 대한 거래를 성공적으로 환불했음을 나타냅니다. 에는 환불된 거래의 타임스탬프가 포함되어 있습니다. 및 원래 거래 및 제품을 식별합니다 . 에는 이유가 포함되어 있습니다.cancellation_date_msoriginal_transaction_idproduct_idcancellation_reason
        $aReceipt = $callBackData['unified_receipt']['latest_receipt_info'];
        for ($i = 0; $i < count($aReceipt); $i++)
        {
            // $aReceipt[$i]['original_transaction_id']
            // transaction_id 로 결제건을 찾아 환불처리해준다. 
        }
    }
    $this->output->set_output("<html><body><RESULT>SUCCESS</RESULT></body></html>");
}

 위 처럼  코드를 구성할수 있다. 

자세한 내용은 https://developer.apple.com/documentation/appstoreservernotifications/notification_type 참조.

 

여기서 소모성 아이템에 대한 환불 요청과 환불에 대한 처리를 했다. 

앱스토어에서 환불 요청시 소비 데이터 요청을 하는 경우에 대한 응답을 해보도록 하겠다.

function sendAppleConsumption($payment_transaction_id)
{
    // transaction_id 로 결제 정보와 사용자의 정보를 불러온다. 
    $buldle_id = "";// 앱번들아이디.

    $msg = array();
    $msg['accountTenure'] = 0; // 0~7 (필수) 고객 계정의 연령입니다.
    $msg['appAccountToken'] = $this->getUuidV4(sprintf("%016d", '회원번호')); // (필수) 소모성 인앱 구매 거래를 완료한 인앱 사용자 계정의 UUID입니다.
    $msg['consumptionStatus'] = 0; // 0~3 (필수) 고객이 인앱 구매를 한 정도를 나타내는 값.
    $msg['customerConsented'] = true; // (필수) 고객이 소비 데이터 제공에 동의했는지 여부를 나타내는 true또는 의 부울 값 입니다.false
    $msg['deliveryStatus'] = 5; // 0~5 // (필수) 앱이 제대로 작동하는 인앱 구매를 성공적으로 전달했는지 여부를 나타내는 값입니다.
    $msg['lifetimeDollarsPurchased'] = 0; // 0~7 (필수) 모든 플랫폼에서 고객이 앱에서 수행한 총 인앱 구매 금액(USD)을 나타내는 값입니다.
    $msg['lifetimeDollarsRefunded'] = 0; // 0~7 (필수) 앱에서 모든 플랫폼에 걸쳐 고객이 받은 환불의 총액을 USD로 나타내는 값입니다.
    $msg['platform'] = 1; // 0~2 (필수) 고객이 인앱 구매를 소비한 플랫폼을 나타내는 값입니다.
                          // 0 선언되지 않음. 1 애플 플랫폼. 2 비 Apple 플랫폼.
    $msg['playTime'] = 0; // 0~7 (필수) 고객이 앱을 사용한 시간을 나타내는 값입니다.
    $msg['sampleContentProvided'] = true; // (필수) 구매 전에 콘텐츠의 무료 샘플 또는 평가판을 제공했는지 또는 해당 기능에 대한 정보를 제공했는지 여부를 나타내는 true또는 의 부울 값 입니다.false
    $msg['userStatus'] = 0; // 0~4 (필수) 고객 계정의 상태입니다.

    if(결제정보가 서버에 저장되었고 사용자 정보가 있는경우) 
    {
        // accountTenure
        // 0 계정 연령이 선언되지 않았습니다.
        // 1 계정 기간은 0~3일입니다.
        // 2 계정 기간은 3~10일입니다.
        // 3 계정 기간은 10~30일입니다.
        // 4 계정 기간은 30~90일입니다.
        // 5 계정 기간은 90~180일입니다.
        // 6 계정 기간은 180~365일입니다.
        // 7 계정 사용 기간이 365일을 초과했습니다.
        $day = floor((abs(time() - strtotime('사용자가입일')) / 60 / 60 / 24));
        if ($day <= 3)
        {
            $msg['accountTenure'] = 1;
        }
        else if ($day <= 10)
        {
            $msg['accountTenure'] = 2;
        }
        else if ($day <= 30)
        {
            $msg['accountTenure'] = 3;
        }
        else if ($day <= 90)
        {
            $msg['accountTenure'] = 4;
        }
        else if ($day <= 180)
        {
            $msg['accountTenure'] = 5;
        }
        else if ($day <= 365)
        {
            $msg['accountTenure'] = 6;
        }
        else
        {
            $msg['accountTenure'] = 7;
        }

        // consumptionStatus
        // 0 소비 상태가 선언되지 않았습니다.
        // 1 인앱 구매는 소비되지 않습니다.
        // 2 인앱 구매가 부분적으로 소비됩니다.
        // 3 인앱 구매가 완전히 소비되었습니다.
        if (충전된 잔여 금액이 환불될 금액 보다 많아 환불가능한경우 cash >= 상품가격)
        {
            $msg['consumptionStatus'] = 1;
        }
        else if (충전 잔여 금액이 적어 환불될 금액보다 적은경우  cash < 상품가격)
        {
            $msg['consumptionStatus'] = 2;
        }
        else
        {
            $msg['consumptionStatus'] = 3;
        }
        
        // deliveryStatus
        // 0 앱에서 소모품 인앱 구매를 전달했으며 제대로 작동합니다.
        // 1 품질 문제로 인해 앱에서 소모품 인앱 구매를 제공하지 않았습니다.
        // 2 앱에서 잘못된 항목을 배송했습니다.
        // 3 서버 중단으로 인해 앱에서 소모성 인앱 구매를 제공하지 않았습니다.
        // 4 게임 내 통화 변경으로 인해 앱에서 소모품 인앱 구매를 제공하지 않았습니다.
        // 5 앱에서 다른 이유로 소모품 인앱 구매를 제공하지 않았습니다.
        $msg['deliveryStatus'] = 0;
        
        // lifetimeDollarsPurchased
        // 0 평생 구매 금액은 미신고입니다.
        // 1 평생 구매 금액은 0 USD입니다.
        // 2 평생 구매 금액은 0.01–49.99 USD입니다.
        // 3 평생 구매 금액은 50–99.99 USD입니다.
        // 4 평생 구매 금액은 100–499.99 USD입니다.
        // 5 평생 구매 금액은 500–999.99 USD입니다.
        // 6 평생 구매 금액은 1000–1999.99 USD입니다.
        // 7 평생 구매 금액은 2000 USD 이상입니다.
        $pay = 총 결제 금액 - 현재 환불 대상 결제 금액
        if ($pay <= 0)
        {
            $msg['lifetimeDollarsPurchased'] = 1;
        }
        else if ($pay <= 55000)
        {
            $msg['lifetimeDollarsPurchased'] = 2;
        }
        else if ($pay <= 110000)
        {
            $msg['lifetimeDollarsPurchased'] = 3;
        }
        else if ($pay <= 550000)
        {
            $msg['lifetimeDollarsPurchased'] = 4;
        }
        else if ($pay <= 1100000)
        {
            $msg['lifetimeDollarsPurchased'] = 5;
        }
        else if ($pay <= 2200000)
        {
            $msg['lifetimeDollarsPurchased'] = 6;
        }
        else
        {
            $msg['lifetimeDollarsPurchased'] = 7;
        }
        
        // lifetimeDollarsRefunded
        // 0 평생 환불 금액은 미신고입니다.
        // 1 평생 환불 금액은 0 USD입니다.
        // 2 평생 환불 금액은 0.01–49.99 USD입니다.
        // 3 평생 환불 금액은 50–99.99 USD입니다.
        // 4 평생 환불 금액은 100–499.99 USD입니다.
        // 5 평생 환불 금액은 500–999.99 USD입니다.
        // 6 평생 환불 금액은 1000–1999.99 USD입니다.
        // 7 평생 환불 금액은 2000 USD 이상입니다.
        
        // playTime
        // 0 참여 시간은 선언되지 않았습니다.
        // 1 참여 시간은 0~5분입니다.
        // 2 참여 시간은 5~60분입니다.
        // 3 참여 시간은 1~6시간입니다.
        // 4 참여 시간은 6~24시간입니다.
        // 5 참여 시간은 1~4일입니다.
        // 6 참여 시간은 4~16일입니다.
        // 7 참여 기간은 16일이 넘습니다.

        // userStatus
        // 0 계정 상태가 선언되지 않았습니다.
        // 1 고객의 계정이 활성 상태입니다.
        // 2 고객의 계정이 정지되었습니다.
        // 3 고객의 계정이 해지됩니다.
        // 4 고객의 계정은 액세스가 제한되어 있습니다.
    }
    return $this->sendConsumption($buldle_id, $payment_transaction_id, json_encode($msg));
}

위에서 appAccountToken 을 제대로 보내지 않으면 서버에서 제대로된 응답을 내려주지 않는다. 

appAccountToken 의 값은 애플 UUID 형식에 맞게 전달해야 한다. 

해당 코드는 아래처럼 하면된다.

function getUuidV4($data = null)
{
    // Generate 16 bytes (128 bits) of random data or use the data passed into the function.
    $data = $data ?? random_bytes(16);
    assert(strlen($data) == 16);

    // Set version to 0100
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
    // Set bits 6-7 to 10
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80);

    // Output the 36 character UUID.
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

32개의 문자와 하이픈 4개로 이루어 진 그룹으로 형성되어야 한다. 

8–4–4–4–12 => 12345678-1234-1234-1234-1234567890ab

 

이후 해당 값을 서버로 전송하고 응답값을 받아 처리하면된다.

// jwt 최신버전에서 사용
// use Firebase\JWT\JWT;
// use Firebase\JWT\Key;

function sendConsumption($bundleId, $originalTransactionId, $msg)
{
    $outData = array();
    $outData['result'] = false;
    $outData['msg'] = '';
    $apple_API_key_id = 'api 용 키페어아이디';
    $keyfile = "file://AuthKey_{$apple_API_key_id}.p8"; // absolute path
    $key = openssl_pkey_get_private($keyfile);

    $data = ['iss' => 'apple_issuer_id','iat' => time(),'exp' => time() + 1200,'aud' => "appstoreconnect-v1",'nonce' => $this->getUuidV4(),'bid' => $bundleId];
    $client_secret = JWT::encode($data, $apple_API_key_id), $key, "ES256");

    $url = "https://api.storekit.itunes.apple.com/inApps/v1/transactions/consumption/{$originalTransactionId}";

    // only needed for PHP prior to 5.5.24
    if (!defined('CURL_HTTP_VERSION_2_0'))
    {
        define('CURL_HTTP_VERSION_2_0', 3);
    }
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($ch, CURLOPT_POST, TRUE);
    curl_setopt($ch, CURLOPT_PORT, 443);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $client_secret","Content-Type: application/json"]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $msg);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_HEADER, 1);
    $response = curl_exec($ch);
    if ($response === FALSE)
    {
        $outData['result'] = false;
        $outData['msg'] = curl_error($ch);
    }
    else
    {
        $outData['result'] = true;
        $outData['msg'] = $response;
        // $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    }
    return $outData;
}

 

이렇게 소모성 아이템에 대한 환불 요청에 대한 응답을 발송하면된다. 

그후 환불이 발생할경우 환불건에 대한 것을 서버에서 아이템 지급 취소나 회수를 하면된다. 

 

JWT 는  https://github.com/firebase/php-jwt  에서 JWT 라이브러리를 다운로드 받아 사용하면된다.

 

 

반응형

'iOS' 카테고리의 다른 글

맥북에어 15 인치 드디어 정식 발표 공개  (2) 2023.06.07
Apple iOS PUSH php jwt 방식 전송.  (0) 2021.12.28
Apple iOS PUSH 인증서 갱신  (0) 2020.01.30