Google Play 미지급 구매 복구
결제는 끝났지만 상품 지급이 마무리되지 않은 Google Play 구매를 조회해 남은 상품 지급과 거래 종료를 완료합니다.
Google Play 결제 구현의 설정과 설치를 먼저 완료하세요.
실행 시점
아래 상황에서 구매가 끝나지 않은 채 남습니다.
- 결제 직후 앱이 종료되었거나 네트워크가 끊긴 경우
- 영수증 검증에는 성공했지만 상품 지급이나 거래 종료가 실패한 경우
- 사용자가 편의점 충전 같은 지연 결제를 나중에 완료한 경우
소비 처리하지 않은 소모성 상품은 다시 구매할 수 없으므로, 앱을 실행할 때와 상점 화면을 열 때 복구를 실행하세요. 두 시점이 겹치더라도 복구가 동시에 두 번 실행되지 않게 하고, 진행 중인 구매와 같은 주문을 동시에 처리하지 마세요. 구매 시작 단계에서 StoreItemAlreadyOwned를 받았을 때도 이 복구를 먼저 실행해야 합니다.
전체 흐름
| 순서 | 구분 | 하는 일 |
|---|---|---|
| 1 | 레시피 코드 | 미지급 구매 조회 |
| 2 | 앱 코드 | 구매 당시 주문 정보 보완 |
| 3 | 레시피 코드 | 구매 정보 준비 |
| 4 | 앱 코드, 앱 서버, Hive Axyl 서버 API | 영수증 검증과 상품 지급 |
| 5 | 레시피 코드 | 거래 종료 |
| 6 | Hive Axyl SDK | 지급 결과 기록 |
1. 미지급 구매 조회
- 레시피 코드 레시피 코드를 앱에서 호출합니다.
FindUndeliveredAsync()를 호출해 아직 소비되지 않은 구매 목록을 받으세요.
조회한 항목은 한 건씩 순서대로 처리하세요.
using Hive.Axyl.Samples.Recipes;
var recipe = new UndeliveredPurchaseRecipe(new GooglePurchaseSource());
var query = new UndeliveredQuery(
country: "KR",
language: "ko",
serverId: "server01",
appVersion: Application.version,
accountUuid: accountUuid);
FindUndeliveredOutcome found = await recipe.FindUndeliveredAsync(query, token);
if (found.Status != FindUndeliveredStatus.Success)
{
Debug.LogError($"{found.FailedStep}: {found.BusinessOutcome} {found.Error?.Message}");
return;
}
foreach (PendingPurchase entry in found.Undelivered)
{
// 2단계부터 한 건씩 순서대로 처리합니다.
}
레시피가 내부에서 수행하는 작업은 아래와 같습니다.
| 구분 | 호출 | 확인할 곳 |
|---|---|---|
| 스토어 플러그인 | StartConnectionAsync()로 BillingClient 연결 | BillingClient 연결 |
| 스토어 플러그인 | QueryPurchasesAsync()로 미소비 구매 목록 조회 | 미지급 영수증 재조회 |
Google Play는 미지급 구매 목록을 기기에 보관합니다. 결제가 완료된 구매만 반환하며, 결제가 진행 중인 구매는 목록에 없습니다. 아직 청구되지 않았으므로 지급할 상품도 없으며, 결제가 완료되면 Play가 다시 통지합니다.
목록이 비어 있으면 마무리할 구매가 없다는 뜻이며 오류가 아닙니다.
2. 구매 당시 주문 정보 보완
- 앱 코드 앱에서 직접 구현합니다.
Play가 보관한 구매 기록에는 결제 금액과 통화, 구매를 시작할 때 전달한 주문 정보가 들어 있지 않습니다. 레시피가 돌려준 항목의 Order.Price는 0, Order.Currency는 빈 값, Order.IapPayload는 null이며, ServerId와 AccountUuid에는 1단계의 조회 조건에 넣은 값이 들어갑니다.
검증 전에 이 값을 구매 당시의 주문 정보로 바꿔 넣으세요. 먼저 Google Play 결제 구현에서 저장한 구매 정보 가운데 StoreTransactionId가 같은 구매를 찾고, 없으면 같은 계정과 상품 ID로 남긴 주문 기록을 사용합니다. 편의점 충전 같은 지연 결제는 구매를 시작할 때 Pending을 받지 못하므로 주문 기록에만 남아 있습니다. 예제 코드의 pendingStore.FindAsync()와 FindOrderRecordAsync()는 앱이 구현하는 메서드입니다.
// 구매 정보, 주문 기록 순서로 구매 당시의 주문 정보를 찾습니다.
PurchaseOrder original =
(await pendingStore.FindAsync(entry.StoreTransactionId))?.Order
?? await FindOrderRecordAsync(accountUuid, entry.Order.ProductId);
if (original == null)
{
// 구매 당시 정보를 찾지 못한 항목은 금액을 추측해 채우지 않고 오류로 기록합니다.
continue;
}
var pending = new PendingPurchase(
entry.AxylReceipt,
entry.OrderId,
original, // 구매 당시의 금액, 통화, 주문 정보
entry.Market,
entry.VerifyToken,
entry.FinishToken,
entry.StoreTransactionId,
entry.StoreVerificationError);
// 보완한 구매 정보를 저장한 뒤 3단계로 진행합니다.
await pendingStore.SaveAsync(pending);
구매 당시 금액을 추측해 채우지 마세요
현재 상품 목록의 가격으로 대신 채우거나 금액을 비워 둔 채 검증하지 마세요. 구매한 뒤에 가격이 바뀌었다면 사용자가 실제로 결제한 금액과 달라지고, 금액을 비워 두면 Hive Axyl 서버가 0을 실제 결제 금액과 대조합니다. 두 기록에서 모두 찾지 못한 항목은 완료하지 않은 채 남기고 오류로 기록하세요. 소비 처리하지 않은 구매는 확인 기간이 지나면 Google이 자동으로 환불합니다.
3. 구매 정보 준비
- 레시피 코드 레시피 코드를 앱에서 호출합니다.
보완한 구매 정보로 PreparePurchaseAsync()를 호출해 구매 결과를 Hive Axyl 서버에 다시 기록하고, 앱 서버에 보낼 구매 정보를 준비하세요. 레시피는 이 단계에서 영수증 검증을 요청하지 않으므로, 성공 결과를 받아도 아직 상품을 지급하면 안 됩니다.
PreparePurchaseOutcome prepared = await recipe.PreparePurchaseAsync(pending, token);
if (prepared.Pending != null)
{
// 준비 단계가 돌려준 값으로 저장한 구매 정보를 교체합니다.
await pendingStore.SaveAsync(prepared.Pending);
}
if (prepared.Status != PreparePurchaseStatus.Success)
{
// 저장한 구매 정보를 그대로 두고 다음 항목으로 넘어갑니다.
continue;
}
거래를 종료할 때 보내는 결제 확정 요청이 이 기록을 기준으로 동작하므로, 복구에서도 구매 결과 저장을 다시 보냅니다. 이미 저장된 구매라 거절되더라도 레시피는 성공으로 처리하며, 지급 여부는 4단계에서 앱 서버가 판단합니다. 레시피가 내부에서 수행하는 작업은 아래와 같습니다.
| 구분 | 호출 | 확인할 곳 |
|---|---|---|
| Hive Axyl SDK | RecordStorePurchaseAsync()로 구매 결과 저장 | 결제 결과 데이터 저장 |
4. 영수증 검증과 상품 지급
-
앱 코드 앱에서 직접 구현합니다.
앱 서버 앱 서버에서 직접 구현합니다.
Hive Axyl 서버 API 앱 서버에서 Hive Axyl 서버 API를 호출합니다.
상세 절차: Hive Axyl Server API로 영수증 검증
복구한 구매도 새 구매와 마찬가지로 앱 서버가 영수증을 검증하고 상품을 지급합니다. 3단계의 prepared.Pending을 앱 서버에 보내 구매 복구로 검증과 지급을 요청하고, 앱 서버가 지급 완료로 응답했을 때만 5단계로 진행하세요.
4.1. 앱 서버에 검증과 지급 요청
예제 코드의 VerifyAndGrantOnceAsync()는 Google Play 결제 구현에서 구현한 앱의 메서드입니다. 복구에서는 두 번째 인자 requestType으로 구매 복구를 나타내는 2를 전달하세요. 사용 예제에서는 이 인자로 PurchaseVerificationKind.Recovery를 전달하고, 앱 서버 연동 코드가 2로 바꿉니다.
// 앱 서버에 구매 복구로 영수증 검증과 상품 지급을 요청합니다.
PurchaseDeliveryConfirmation delivery = await VerifyAndGrantOnceAsync(
prepared.Pending,
requestType: 2, // 2: 구매 복구. 사용 예제의 PurchaseVerificationKind.Recovery
token);
if (delivery == null)
{
// 앱 서버가 지급을 확인하지 않았습니다.
// 저장한 구매 정보를 그대로 두고 다음 항목으로 넘어갑니다.
continue;
}
4.2. 앱 서버의 영수증 검증과 상품 지급
앱 서버는 Google Play 결제 구현과 같은 방법으로 영수증을 검증하고 상품을 지급하세요. 새 구매와 다른 점은 아래와 같습니다.
- 영수증 검증 요청의
requestType에 구매 복구를 나타내는2를 넣습니다. - 새 구매와 같은 지급 기록에서 이미 지급한 구매인지 확인합니다.
requestType은 중복 지급을 막는 기준이 아닙니다. - 이미 지급한 구매라면 다시 지급하지 않고 지급 완료로 응답합니다. 지급 완료로 응답해야 앱이 5단계에서 거래를 종료합니다.
2단계에서 구매 당시의 주문 정보를 채웠다면 Order.IapPayload에는 구매를 시작할 때 전달한 iapPayload가 들어 있습니다. 검증 요청에 이 값을 넣으면 응답의 hiveAxylPayload로 그대로 돌아오므로, 어느 지급 대상이나 서버에 지급해야 하는지를 이 값으로 판단하세요.
5. 거래 종료
- 레시피 코드 레시피 코드를 앱에서 호출합니다.
앱 서버가 지급 완료로 응답하면 ClosePurchaseAsync()를 호출해 구매를 종료하세요. 종료하지 않으면 같은 상품을 다시 판매할 수 없고 다음 복구에서 또 조회됩니다.
레시피가 내부에서 수행하는 작업은 아래와 같습니다.
| 구분 | 호출 | 확인할 곳 |
|---|---|---|
| Hive Axyl SDK | RequestPurchaseAsync()로 결제 확정 | 결제 확정 요청 |
| 스토어 플러그인 | ConsumeAsync()로 Play에서 소비 처리 | 소모성 상품 거래 완료 처리 |
6. 지급 결과 기록
-
Hive Axyl SDK Hive Axyl SDK를 앱에서 호출합니다.
상세 절차: 상품 지급 결과 저장
지급 결과를 Hive Axyl 서버에 기록하세요. 새 구매를 처리할 때와 같습니다.
AxylTransactionId에는 4.1의 VerifyAndGrantOnceAsync()가 앱 서버 응답에서 따로 보관한 hiveAxylTransactionId를 넣으세요. 이 값은 PurchaseDeliveryConfirmation에 담기지 않습니다.
실패 처리
복구는 여러 건을 순회하므로 한 건이 실패해도 나머지를 계속 처리하세요. 상황별로 앱이 할 일은 아래와 같습니다.
- 조회 결과가 비어 있음: 마무리할 구매가 없는 정상 상태
- 조회 실패: 빈 목록과 구분해야 하는 상태. 오류를 기록하고 다음 실행 때 다시 조회
- 준비 실패: 저장한 구매 정보를 그대로 두고 다음 항목으로 진행
- 앱 서버의 지급 미확인: 저장한 구매 정보를 그대로 두고 다음 항목으로 진행
- 종료 실패: 검증과 지급은 이미 끝난 상태. 다음 복구에서 다시 조회되므로 그대로 두어도 됨
BusinessOutcome의 값별 대응 방법은 결제 레시피의 거절 사유를 참조하세요.
다음 단계
새 구매를 구현하는 절차는 Google Play 결제 구현을 참조하세요.