# 원스토어 인앱결제 적용하기

## 개요

본 문서에서는 원스토어 인앱결제 SDK를 이용하여 앱에 원스토어 인앱결제를 추가하는 방법을 설명합니다.\
인앱결제 SDK는 좀 더 편리하고 직관적인 형태로 사용할 수 있도록 `PurchaseClient` 클래스를 제공합니다.\
개발 프로젝트에 SDK를 추가하는 방법은 다음의 단계를 참고해주세요.

## 프로젝트 설정 <a href="#id-sdk" id="id-sdk"></a>

### 종속 항목 추가

* 인앱결제 SDK 라이브러리 파일을 다운로드 한 후 해당 파일을 개발사 앱 프로젝트 내 `libs` 폴더 안에 복사합니다.&#x20;
* [InApp SDK 라이브러리 다운로드](https://github.com/ONE-store/onestore_iap_release/tree/iap19-release/android_app_sample/app/libs)
* 앱의 `build.gradle` 파일의 종속성 섹션에 다음 행을 추가합니다.&#x20;

```gradle
dependencies {
    ...
    implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}
```

### \<queries> 설정

`AndroidManifest.xml` 파일에 `<queries>`를 설정 해야합니다.\
자세한 내용은 [공지사항](https://dev.onestore.co.kr/devpoc/support/news/noticeView.omp?noticeId=32968)을 참조하세요.

{% hint style="danger" %}
**\<queries>** 태그를 설정하지 않으면 SDK에서 원스토어 서비스를 찾을 수 없습니다.
{% endhint %}

```xml
<manifest>
    <!-- 
        if your binary use ONE store's In-app SDK,
        Please make sure to declare the following query on Androidmanifest.xml. 
        Refer to the notice for more information.
        https://dev.onestore.co.kr/devpoc/support/news/noticeView.omp?noticeId=32968
     -->
    <queries>
        <intent>
            <action android:name="com.onestore.ipc.iap.IapService.ACTION" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="onestore" />
        </intent>
    </queries>
    ...
    <application>
        ...
    </application>
</manifest>
```

### 글로벌 스토어 배포를 위한 테스트 옵션 설정

SDK가 강제로 글로벌 스토어로 연동되도록 하는 옵션입니다.\
이 기능은 **SDK v19.01.00 버전**부터 적용되었습니다.

```xml
<manifest>
    ...
    <application>
        <activity>
            ...
        </activity>
        ...
        
        <!-- Options for in-app testing on your global store -->
        <meta-data android:name="onestore:dev_option" android:value="global" />
    </application>
</manifest>
```

{% hint style="danger" %}
배포 빌드 버전에서는 이 옵션을 **반드시 삭제**해야 합니다.
{% endhint %}

## 인앱 라이브러리 적용하기

### 로그레벨 설정

개발 단계에서 로그 레벨을 설정하여 SDK의 데이터의 흐름을 좀 더 자세히 노출할 수 있습니다.\
[`android.util.Log`](https://developer.android.com/reference/android/util/Log#summary)에 정의된 값을 기반으로 동작합니다.

```kotlin
/**
 * Set the log level.<br/>
 * {@link Log#VERBOSE}, {@link Log#DEBUG}, {@link Log#INFO},
 * {@link Log#WARN}, {@link Log#ERROR}
 * @param level int
 */
com.gaa.sdk.iap.Logger.setLogLevel(2)
```

| 상수             | 값 |
| -------------- | - |
| VERBOSE        | 2 |
| DEBUG          | 3 |
| INFO (default) | 4 |
| WARN           | 5 |
| ERROR          | 6 |

{% hint style="danger" %}
배포 빌드 버전에서는 보안에 취약할 수 있으니 이 옵션을 **삭제**해야 합니다.
{% endhint %}

### 오류 처리 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

원스토어 결제 라이브러리는 `IapResult` 형식으로 오류를 반환합니다. `IapResult`에는 앱에서 발생할 수 있는 결제 관련 오류를 분류하는 `ResponseCode`가 포함되어 있습니다. 예를 들어 `RESULT_NEED_LOGIN` 이나 `RESULT_NEED_UPDATE` 같은 오류 코드가 수신되면 앱에서 그에 맞는 처리를 해야 합니다.

### PurchaseClient 초기화 및 연결하기

원스토어에 결제를 요청 하려면 먼저 다음 작업을 통해 원스토어 서비스에 연결해야 합니다.

1. `PurchaseClient.newBuilder()`를 호출하여 `PurchaseClient` 인스턴스를 만듭니다. `PurchaseClient` 생성 시에 현재 `Activity`의 `Context` 정보를 넣습니다.\
   또한 `setListener()`에 `PurchaseUpdatedListener`를 등록하여 구매에 대한 값을 전달 받아야 합니다.\
   `setBase64PublicKey()`는 옵션 값이지만, 사용하는 것을 권장합니다.  public key를 넣었을 경우 SDK에서 구매 데이터 위변조에 대한 서명 확인 작업을 합니다.\
   라이선스 키(public key)는 원스토어 개발자 센터에서 확인 가능합니다.\ <br>

   <div data-gb-custom-block data-tag="hint" data-style="success" class="hint hint-success"><p>라이선스 키의 경우, 앱 내 코드로 저장하기 보다는 보안이 될 수 있도록 서버 등을 이용하여 전달받아 사용하는 것을 권장합니다.</p></div>

   <figure><img src="https://1837360763-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fot0z57AnnXZ02C5qyePV%2Fuploads%2FDtjDqTZIYPn97G6kCFTH%2Fimage.png?alt=media&#x26;token=c50a9479-982c-44ed-8070-3676c30ea6ee" alt=""><figcaption></figcaption></figure>
2. `startConnection`을 통해 원스토어 서비스와 연결합니다. 클라이언트 설정이 완료되고 추가로 요청할 준비가 되면 응답을 받을 수 있도록 `PurchaseClientStateListener`를 구현해야 합니다.<br>
3. 클라이언트 연결이 끊어진 경우 `onServiceDisconnected()`가 호출되며, 끊어진 원스토어 서비스와 연결 할 수 있도록 응답 메서드를 재정의하고 자체적으로 연결을 재시도 할 수 있도록 구현합니다. 예를 들어 장시간 백그라운드에 놓여 있을 경우 `PurchaseClient`의 연결이 끊어질 수 있습니다. 이 경우 추가 요청을 하기 전에 `PurchaseClient.startConnection()` 메서드를 호출하여 다시 연결해야 합니다.&#x20;

다음은 연결을 시작하고 사용할 준비가 되었는지 테스트하는 방법을 나타내는 예제입니다.&#x20;

{% hint style="info" %}
앱과 원스토어 서비스 간의 연결이 끊어지면, 자체적으로 연결이 재시도 되도록 `onServiceDisconnectied()` 메서드를 재정의하는 것이 좋습니다.

`PurchaseClient`의 모든 메서드는 연결이 유지된 상태로 실행되어야 합니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
private lateinit var mPurchaseClient: PurchaseClient
...
mPurchaseClient = PurchaseClient.newBuilder(activity)
                       .setListener(this)
                       .setBase64PublicKey(/*your public key*/) // optional
                       .build()
                       
mPurchaseClient.startConnection(object : PurchaseClientStateListener {
    override fun onSetupFinished(iapResult: IapResult) {
        if (iapResult.isSuccess) {
            // The PurchaseClient is ready. You can query purchases here.
        } else if (iapResult.responseCode == ResponseCode.RESULT_NEED_LOGIN) {
            // The connection is completed successfully but login is required. You must write your login code here.
        } else if (iapResult.responseCode == ResponseCode.RESULT_NEED_UPDATE) {
            // You need the required version of the ONE store service in the SDK.
        } else {
            // Other error codes.
        }
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
private PurchaseClient mPurchaseClient;
...
mPurchaseClient = PurchaseClient.newBuilder(activity)
                       .setListener(this)
                       .setBase64PublicKey(/*your public key*/) // optional
                       .build();
                       
mPurchaseClient.startConnection(new PurchaseClientStateListener() {
    @Override
    public void onSetupFinished(IapResult iapResult) {
        if (iapResult.isSuccess()) {
            // The PurchaseClient is ready. You can query purchases here.
        } else if (iapResult.getResponseCode() == ResponseCode.RESULT_NEED_LOGIN) {
            // The connection is completed successfully but login is required. You must write your login code here.
        } else if (iapResult.getResponseCode() == ResponseCode.RESULT_NEED_UPDATE) {
            // You need the required version of the ONE store service in the SDK.
        } else {
            // Other error codes.
        }
    }
 
    @Override
    public void onServiceDisconnected() {
        // Try to restart the connection on the next request to
        // ONE store service by calling the startConnection() method.
    }
});
```

{% endtab %}
{% endtabs %}

그리고 `Activity` 가 종료될 때 `onDestory()` 메서드에서 `PurchaseClient`를 종료하는 코드를 넣어야 합니다. 바인딩된 서비스를 해제하지 않으면 생성된 연결이 계속 유지되면서 어플리케이션 성능에 문제가 생길 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onDestory() {
    super.onDestory()
    mPurchaseClient.endConnection()
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
protected void onDestroy() {
    super.onDestroy();
    if (mPurchaseClient != null) {
        mPurchaseClient.endConnection();
        mPurchaseClient = null;
    }
}
```

{% endtab %}
{% endtabs %}

### 상품 상세정보 조회하기 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

상품 상세정보를 조회하려면 `queryProductDetailsAsync()`를 호출합니다. 사용자에게 제품을 표시하기 전에 중요한 단계입니다.

`queryProductDetailsAsync()`를 호출할 때 `setProductType()`과 함께 원스토어 개발자 센터에서 생성된 인앱 상품 ID 문자열 목록을 지정하는 `ProductDetailParams`의 인스턴스를 전달합니다.

`ProductType`은 아래와 같습니다.

<table><thead><tr><th width="246">Product</th><th>Enum</th></tr></thead><tbody><tr><td>관리형 상품</td><td><code>ProductType.INAPP</code></td></tr><tr><td>월 정액 상품</td><td><code>ProductType.AUTO</code></td></tr></tbody></table>

위의 모든 유형의 데이터를 한 번에 조회하고 싶을 경우는 `ProductType.ALL` 설정하면 됩니다.

{% hint style="warning" %}
`ProductType.ALL`은 [상품 상세정보 조회하기](#id-04.-sdk-1)에서만 사용할 수 있으며, 구매 요청하기, 구매 내역 조회하기 에서는 사용할 수 없습니다.
{% endhint %}

비동기 작업 결과를 처리하려면 `ProductDetailsListener` 인터페이스를 구현해야 합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val params = ProductDetailsParams.newBuilder()
        .setProductIdList(productIdList)
        .setProductType(ProductType.INAPP)
        .build()
        
purchaseClient.queryProductDetailsAsync(params) { iapResult, productDetails -> 
    // Process the result.
}
```

{% endtab %}

{% tab title="Java" %}
{% code fullWidth="false" %}

```java
ProductDetailsParams params = ProductDetailsParams.newBuilder()
        .setProductIdList(productIdList)
        .setProductType(ProductType.INAPP)
        .build();

purchaseClient.queryProductDetailsAsync(params, new ProductDetailsListener() {
    @Override
    public void onProductDetailsResponse(IapResult iapResult, List<ProductDetail>) {
        // Process the result. 
    }
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 구매 요청하기 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

앱에서 구매 요청을 하기 위해서는 기본 스레드에서 `launchPurchaseFlow()` 함수를 호출합니다.

이 함수는 `queryProductDetailsAsync()` 함수를 호출해서 얻은 `ProductDetail` 객체의 값을 토대로 `PurchaseFlowParams` 객체를 생성합니다.\
`PurchaseFlowParams` 객체를 생성하려면 `PurchaseFlowParams.Builder` 클래스를 사용합니다.

`setDeveloperPayload()`는 개발사에서 임의로 입력한 값으로 최대 200byte입니다. 이 값은 결제 후에 데이터의 정합성과 부가 데이터를 확인하기 위해 사용할 수 있습니다.\
`setProductName()`은 상품 이름을 결제 시 변경하여 노출하고 싶을 때 사용됩니다.\
`setQuantity()`는 관리형 인앱 상품에만 적용되며 한 상품을 여러 개 구매할 때 사용됩니다.

{% hint style="success" %}
원스토어는 사용자에게 할인 쿠폰, 캐시백 등의 다양한 혜택 프로모션을 진행하고 있습니다.\
개발사는 구매 요청 시에 `gameUserId`, `promotionApplicable` 파라미터를 이용하여 앱을 사용하는 유저의 프로모션 참여를 제한하거나 허용할 수 있습니다.\
개발사는 앱의 고유한 유저 식별 번호 및 프로모션 참여 여부를 선택하여 전달하고, 원스토어는 이 값을 토대로 사용자의 프로모션 혜택을 적용하게 됩니다.
{% endhint %}

{% hint style="warning" %}
`gameUserId`, `promotionApplicable` 파라미터는 옵션 값으로 원스토어 사업 부서 담당자와 프로모션에 대해 사전협의가 된 상태에서만 사용하여야 하며, 일반적인 경우에는 값을 보내지 않습니다.\
또한, 사전협의가 되어 값을 보낼 경우에도 개인 정보보호를 위해 `gameUserId`는 *hash*된 고유한 값으로 전송하여야 합니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val activity: Activity = ...

val purchaseFlowParams = PurchaseFlowParams.newBuilder()
      .setProductId(productId)
      .setProductType(productType)
      .setDeveloperPayload(devPayload)    // optional
      .setQuantity(1)                     // optional
      .setProductName("")                 // optional
      .setGameUserId("")                  // optional
      .setPromotionApplicable(false)      // optional
      .build()

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams)
```

{% endtab %}

{% tab title="Java" %}

```java
Activity activity = ...

PurchaseFlowParams purchaseFlowParams = PurchaseFlowParams.newBuilder()
      .setProductId(productId)
      .setProductType(productType)
      .setDeveloperPayload(devPayload)    // optional
      .setQuantity(1)                     // optional
      .setProductName("")                 // optional
      .setGameUserId("")                  // optional
      .setPromotionApplicable(false)      // optional
      .build();

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams);
```

{% endtab %}
{% endtabs %}

`launchPurchaseFlow()` 호출에 성공하면 아래와 같은 화면을 표시합니다. \[그림 1]은 정기 결제 구매 화면을 나타냅니다.<br>

<figure><img src="https://1837360763-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fot0z57AnnXZ02C5qyePV%2Fuploads%2FSKZA4F4vqSbTB6w8qgOY%2Fimage.png?alt=media&#x26;token=10afbf89-8aca-44b4-ba1d-4d548578546b" alt=""><figcaption><p>[그림 1]</p></figcaption></figure>

구매에 성공하면 `PurchasesUpdatedListener` 인터페이스의 `onPurchasesUpdated()` 함수에 구매 작업 결과가 전송됩니다. 이 리스너는 [PurchaseClient 초기화 및 연결하기](#purchaseclient) 할 때 `setListener()` 함수를 사용하여 지정됩니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onPurchasesUpdated(iapResult: IapResult, purchases: List<PurchaseData>?) {
    if (iapResult.isSuccess && purchases != null) {
        for (purchase in purchases) {
            handlePurchase(purchase)
        }
    } else if (iapResult.responseCode == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.responseCode == ReponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onPurchasesUpdated(IapResult iapResult, List<PurchaseData> purchases) {
    if (iapResult.isSuccess() && purchases != null) {
        for (purchase in purchases) {
            handlePurchase(purchase);
        }
    } else if (iapResult.getResponseCode() == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.getResponseCode() == ReponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
```

{% endtab %}
{% endtabs %}

\
구매가 성공하면 구매 데이터에는 사용자 및 상품 ID를 나타내는 고유 식별자인 구매 토큰도 생성됩니다. 구매 토큰을 앱 내에 저장할 수 있지만 구매를 인증하고 사기로부터 보호할 수 있는 백엔드 서버로 토큰을 전달하는 것이 좋습니다.

<figure><img src="https://1837360763-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fot0z57AnnXZ02C5qyePV%2Fuploads%2FitO6sDFiGQ3VmNiQzMxk%2Fimage.png?alt=media&#x26;token=12b485ca-5e48-4f1d-8623-4026c6085f6e" alt=""><figcaption></figcaption></figure>

관리형 상품과 정기 결제 상품의 구매 토큰은 결제가 일어날 때마다 구매 토큰이 발행됩니다. (월정액 상품의 경우 자동 결제가 갱신되는 동안 구매 토큰은 동일하게 유지됩니다.)

또한 사용자는 영수증 번호가 포함된 거래 영수증을 이메일로 받습니다. 관리형 상품은 구매할 때마다 이메일을 받으며, 월정액 상품과 정기 결제 상품은 처음 구매 시 그리고 이후 갱신될 때마다 이메일을 받습니다.

### 구매 처리하기

구매가 완료되면 앱에서 구매 확인 처리를 해야 합니다. 대부분의 경우 앱은 `PurchasesUpdatedListener`를 통해 구매 알림을 받습니다.\
또는 [구매 내역 조회하기](#undefined-12)에서 설명된 것처럼 앱이 `PurchaseClient.queryPurchasesAsync()` 함수를 호출하여 처리하는 경우가 있습니다.

다음 메서드 중 하나를 사용해 구매를 확인할 수 있습니다.

* 소모성 제품인 경우 `PurchaseClient.consumeAsync()`를 사용합니다.
* 소모성 제품이 아니라면 `PurchaseClient.acknowledgeAsync()`를 사용합니다.

#### 관리형 상품 소비하기 (consume) <a href="#id-04.-sdk-consume" id="id-04.-sdk-consume"></a>

관리형 상품은 소비를 하기 전까지는 재구매 할 수 없습니다.

상품을 소비하기 위해서는 `consumeAsync()`를 호출합니다. 또한 소비 작업 결과를 전달받으려면 `ConsumeListener` 인터페이스를 구현해야 합니다.

{% hint style="info" %}
관리형 상품을 소비하지 않으면 영구성 형태의 상품 타입처럼 활용할 수 있으며, 구매 후 즉시 소비하면 소비성 형태의 상품으로도 활용됩니다.\
또한 특정 기간 이후에 소비하면 기간제 형태의 상품으로 활용할 수 있습니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
fun handlePurchase(purchase: PurchaseData) {
    // Purchase retrieved from PurchaseClient#queryPurchasesAsync
    // or your PurchasesUpdatedListener.
    val purchase: PurchaseData = ...
      
    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    val consumeParams = ConsumeParams.newBuilder()
                            .setPurchaseData(purchase)
                            .build()
                            
    purchaseClient.consumeAsync(consumeParams) { iapResult, purchaseData -> 
        // Process the result.
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
private void handlePurchase(PurchaseData purchase) {
    // Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
    PurchaseData purchase = ...
      
    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    ConsumeParams consumeParams = ConsumeParams.newBuilder()
                                        .setPurchaseData(purchase)
                                        .build();
                                        
    purchaseClient.consumeAsync(consumeParams, new ConsumeListener() {
        @Override
        public void onConsumeResponse(IapResult iapResult, PurchaseData purchaseData) {
             // Process the result.
        }
    });
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
소비 요청이 때로 실패할 수 있으므로 보안 백엔드 서버를 확인하여 각 구매 토큰이 사용되지 않았는지 확인해야 합니다. 그래야 앱이 동일한 구매에 대해 여러 번 자격을 부여하지 않습니다. 또는 자격을 부여하기 전에 성공적인 소비 응답을 받을 때까지 기다릴 수 있습니다.
{% endhint %}

{% hint style="danger" %}
3일 이내에 구매를 확인(`acknowledge`) 또는 소비(`consume`)를 하지 않으면 사용자에게 상품이 지급되지 않았다고 판단되어 자동으로 환불됩니다.
{% endhint %}

#### 구매 확인하기 (acknowledge) <a href="#id-04.-sdk-acknowledge" id="id-04.-sdk-acknowledge"></a>

비 소비성 상품에 대해 구매 확인 처리를 하려면 `PurchaseClient.acknowledgeAsync()` 함수를 사용합니다. 관리형 상품,  월정액 상품, 구독형 상품 모두 사용할 수 있습니다.

`PurchaseData.isAcknowledged()` 함수를 사용하여 구매 확인 되었는지를 판단 할 수 있습니다. 또한 구매 확인에 대한 작업 결과를 전달받으려면 `AcknowledgeListener` 인터페이스를 구현해야 합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
fun handlePurchase(PurchaseData purchase) {
    if (purchase.purchaseState == PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged) {
            val acknowledgeParams = AcknowledgeParams.newBuilder()
                                        .setPurchaseData(purchase)
                                        .build()
                                        
            purchaseClient.acknowledgeAsync(acknowledgeParams) { iapResult, purchaseData ->
                 // PurchaseClient by calling the queryPurchasesAsync() method.
            }
        }
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
private void handlePurchase(purchase: PurchaseData) {
    if (purchase.getPurchaseState() == PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged()) {
            AcknowledgeParams acknowledgeParams = AcknowledgeParams.newBuilder()
                                                        .setPurchaseData(purchase)
                                                        .build();
                                                        
            purchaseClient.acknowledgeAsync(acknowledgeParams, new AcknowledgeListener() {
                @Override
                public void onAcknowledgeResponse(IapResult iapResult, PurchaseData purchaseData) {
                    // PurchaseClient by calling the queryPurchasesAsync() method.
                }
            });
        }
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
`AcknowledgeListener.onAcknowledgeResponse()` 함수로 전달된 `PurchaseData`는 요청할 당시의 데이터이기 때문에 `acknowledgeState` 값이 변경되지 않습니다. [구매 내역 조회하기](#undefined-12)를 통해 변경된 데이터로 교체해야 합니다.
{% endhint %}

### 구매 내역 조회하기

`PurchasesUpdatedListener`를 사용하여 구매를 처리하는 것만으로는 모든 구매가 처리 되었다는 것을 보장할 수 없습니다. 앱에서 구매 추적을 놓치거나 구매를 인식하지 못할 수 있는 몇 가지 시나리오는 다음과 같습니다.

아래와 같은 시나리오에서 앱은 구매 응답을 받지 못하거나 구매를 인식하지 못하는 경우가 발생할 수 있습니다.&#x20;

* **네트워크 문제**: 사용자가 성공적으로 구매를 하였고, 원스토어에서 확인도 받았지만 기기가 `PurchasesUpdatedListener`를 통해 구매 알림을 받기 전에 네트워크 연결이 끊어졌을 경우
* **여러 기기**: 한 기기에서 항목을 구입한 후 다른 기기로 전환하는 경우&#x20;

이러한 경우를 대비하여 앱의 `onCreate()`*나* `onResume()`에서 `PurchaseClient.queryPurchasesAsync()`를 호출하여 구매가 성공적으로 처리 되었는지 확인해야 합니다.

콜백 처리는 `PurchasesUpdatedListener` 와 동일합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val purchasesListener = PurchasesListener { iapResult, purchases -> 
    if (iapResult.isSuccess && purchases != null) {
        for (purchase in purchases) {
            handlePurchase(purchase)
        }
    } else if (iapResult.responseCode == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.responseCode == ReponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
purchaseClient.queryPurchasesAsync(ProductType.INAPP, purchasesListener)
```

{% endtab %}

{% tab title="Java" %}

```java
PurchasesListener purchasesListener = new PurchasesListener() {
    @Override
    public void onPurchasesResponse(IapResult iapResult, List<PurchaseData> purchases) { 
        if (iapResult.isSuccess() && purchases != null) {
            for (purchase in purchases) {
                handlePurchase(purchase)
            }
        } else if (iapResult.getResponseCode() == ResponseCode.NEED_UPDATE) {
            // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
        } else if (iapResult.getResponseCode() == ReponseCode.NEED_LOGIN) {
            // PurchaseClient by calling the launchLoginFlow() method.
        } else {
             // Handle any other error codes.
        }
    }
};
purchaseClient.queryPurchasesAsync(ProductType.INAPP, queryPurchasesListener);
```

{% endtab %}
{% endtabs %}

### 월정액 상품 상태 변경하기 <a href="#id-04.-sdk-deprecated" id="id-04.-sdk-deprecated"></a>

월정액 상품은 최초 구매 후 30일 갱신이 이루어지는 상품입니다.&#x20;

상태 정보는 [구매 내역 조회하기](#undefined-12)에서 얻어온 `PurchaseData.getRecurringState()`를 통해 확인할 수 있습니다.

월정액 상품의 상태를 변경하려면 `PurchaseClient.manageRecurringProductAsync()`를 사용합니다.\
`RecurringProductParams` 객체에 구매 데이터와 변경하려는 `PurchaseClient.RecurringAction` 값을 입력합니다.&#x20;

{% hint style="info" %}
월정액 상품 갱신 주기

월정액 상품의 자동 결제는 구매한 날짜와 같은 날짜에 진행 됩니다. 단, 익월 같은 날짜가 없는 경우에는 마지막 날짜에 진행됩니다.

* 1월 31일 첫 결제 시
* 2월 28일 또는 29일 갱신
* 3월 28일 또는 29일 갱신
  {% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
fun manageRecurring(purchase: PurchaseData) {
    val recurringParams = RecurringProductParams.newBuilder()
            .setPurchaseData(purchase)
            .setRecurringAction(RecurringAction.CANCEL | RecurringAction.REACTIVATE)
            .build()

    purchaseClient.manageRecurringProductAsync(recurringParams) { iapResult, purchaseData, action ->
        // PurchaseClient by calling the queryPurchasesAsync() method.
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
private void manageRecurring(PurchaseData purchase) {
    RecurringProductParams recurringParams = RecurringProductParams.newBuilder()
            .setPurchaseData(purchase)
            .setRecurringAction(RecurringAction.CANCEL | RecurringAction.REACTIVATE)
            .build();

    purchaseClient.manageRecurringProductAsync(recurringParams, new RecurringProductListener() {
        @Override
        public void onRecurringResponse(IapResult iapResult, PurchaseData purchaseData, String action) {
            // PurchaseClient by calling the queryPurchasesAsync() method.
        }
    });
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
`RecurringProductListener.onRecurringResponse()` 함수로 전달된 `PurchaseData`는 요청할 당시의 데이터이기 때문에 `recurringState` 값이 변경되지 않습니다. [구매 내역 조회하기](#undefined-12)를 통해 변경된 데이터로 교체해야 합니다.
{% endhint %}

### 원스토어 로그인 요청하기

원스토어 인앱 SDK는 사용자가 원스토어에 로그인이 되어 있어야 동작합니다.

이전에 로그인한 이력이 있으면 원스토어에서 자동으로 로그인을 하지만, 원스토어의 로그인이 필요한 경우에는 `PurchaseClient` 인스턴스에서 요청하는 대부분의 API의 응답 리스너에서 `IapResult.getResponseCode()`가 `RESULT_NEED_LOGIN`으로 전달됩니다.

`RESULT_NEED_LOGIN`이 전달된 경우 로그인을 요청하는 메서드를 수행해야 정상적으로 인앱 SDK를 사용할 수 있습니다.

다음은 원스토어 로그인을 요청하는 예제입니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
purchaseClient.launchUpdateOrInstallFlow(activity) { iapResult ->
    if (iapResult.isSuccess) {
        // If the installation is completed successfully,
        // you should try to reconnect with the ONE store service.
        startConnection()
    } else {
        ...
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
purchaseClient.launchUpdateOrInstallFlow(activity, new IapResultListener() {
    @Override
    public void onResponse(IapResult iapResult) {
        if (iapResult.isSuccess()) {
        // If the installation is completed successfully,
        // you should try to reconnect with the ONE store service.
            startConnection();
        } else {
            ...
        }
    }
});
```

{% endtab %}
{% endtabs %}

### 마켓 구분 코드 얻기 <a href="#id" id="id"></a>

IAP 라이브러리 V6부터 Server to Server API를 사용하기 위해서는 마켓 구분 코드가 필요합니다.

`getStoreInfoAsync()`를 통해서 마켓 구분 코드를 획득 할 수 있습니다.

다음은 마켓 구분 코드를 획득 하는 방법을 나타낸 예제입니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
purchaseClient.getStoreInfoAsync { iapResult, storeCode ->
    // Save storecode and use it in Server to Server API.
}
```

{% endtab %}

{% tab title="Java" %}

```java
purchaseClient.getStoreInfoAsync(new StoreInfoListener() {
    @Override
    public void onStoreInfoResponse(IapResult iapResult, String storeCode) {
        // Save storecode and use it in Server to Server API.
    }
});
```

{% endtab %}
{% endtabs %}

### 에러 코드 정의

<table data-full-width="false"><thead><tr><th width="348">상수</th><th width="84">값</th><th>설명</th></tr></thead><tbody><tr><td>RESULT_OK</td><td>0</td><td>성공</td></tr><tr><td>RESULT_USER_CANCELED</td><td>1</td><td>결제가 취소되었습니다.</td></tr><tr><td>RESULT_SERVICE_UNAVAILABLE</td><td>2</td><td>단말 또는 서버 네트워크 오류가 발생하였습니다.</td></tr><tr><td>RESULT_BILLING_UNAVAILABLE</td><td>3</td><td>구매 처리 과정에서 오류가 발생하였습니다.</td></tr><tr><td>RESULT_ITEM_UNAVAILABLE</td><td>4</td><td>상품이 판매중이 아니거나 구매할 수 없는 상태입니다.</td></tr><tr><td>RESULT_DEVELOPER_ERROR</td><td>5</td><td>올바르지 않은 요청입니다.</td></tr><tr><td>RESULT_ERROR</td><td>6</td><td>정의되지 않은 기타 오류가 발생했습니다.</td></tr><tr><td>RESULT_ITEM_ALREADY_OWNED</td><td>7</td><td>이미 아이템을 소유하고 있습니다.</td></tr><tr><td>RESULT_ITEM_NOT_OWNED</td><td>8</td><td>아이템을 소유하고 있지 않아 소비 할 수 없습니다.</td></tr><tr><td>RESULT_FAIL</td><td>9</td><td>결제에 실패했습니다. 결제 가능 여부 및 결제 수단 확인 후 다시 결제해주세요.</td></tr><tr><td>RESULT_NEED_LOGIN</td><td>10</td><td>스토어 앱 로그인이 필요합니다.</td></tr><tr><td>RESULT_NEED_UPDATE</td><td>11</td><td>결제 모듈의 업데이트가 필요합니다.</td></tr><tr><td>RESULT_SECURITY_ERROR</td><td>12</td><td>비정상 앱에서 결제가 요청되었습니다.</td></tr><tr><td>RESULT_BLOCKED_APP</td><td>13</td><td>요청이 차단되었습니다.</td></tr><tr><td>RESULT_NOT_SUPPORT_SANDBOX</td><td>14</td><td>테스트 환경에서는 지원하지 않는 기능입니다.</td></tr><tr><td>ERROR_DATA_PARSING</td><td>1001</td><td>응답 데이터 파싱 오류가 발생했습니다.</td></tr><tr><td>ERROR_SIGNATURE_VERIFICATION</td><td>1002</td><td>구매정보의 서명 검증 에러 발생했습니다.</td></tr><tr><td>ERROR_ILLEGAL_ARGUMENT</td><td>1003</td><td>정상적이지 않 파라미터 입력 되었습니다.</td></tr><tr><td>ERROR_UNDEFINED_CODE</td><td>1004</td><td>정의되지 않는 오류가 발생했습니다.</td></tr><tr><td>ERROR_SIGNATURE_NOT_VALIDATION</td><td>1005</td><td>입력하신 라이센스 키가 유효하지 않습니다.</td></tr><tr><td>ERROR_UPDATE_OR_INSTALL</td><td>1006</td><td>결제 모듈 설치에 실패하였습니다.</td></tr><tr><td>ERROR_SERVICE_DISCONNECTED</td><td>1007</td><td>결제 모듈의 연결이 끊어졌습니다.</td></tr><tr><td>ERROR_FEATURE_NOT_SUPPORTED</td><td>1007</td><td>지원하지 않는 기능입니다.</td></tr><tr><td>ERROR_SERVICE_TIMEOUT</td><td>1009</td><td>서비스와 통신하는 시간이 초과되었습니다.</td></tr><tr><td>RESULT_EMERGENCY_ERROR</td><td>99999</td><td>서버 점검중입니다.</td></tr></tbody></table>
