콘텐츠로 이동

Apple App Store 미지급 구매 복구

결제는 끝났지만 상품 지급이 마무리되지 않은 Apple App Store 구매를 조회해 남은 상품 지급과 거래 종료를 완료합니다.

Apple App Store 결제 구현의 설정과 설치를 먼저 완료하세요.

실행 시점

아래 상황에서 구매가 끝나지 않은 채 남습니다.

  • 결제 직후 앱이 종료되었거나 네트워크가 끊긴 경우
  • 영수증 검증에는 성공했지만 상품 지급이나 거래 완료 처리가 실패한 경우
  • 보호자 승인을 기다리던 결제가 나중에 승인된 경우

완료 처리하지 않은 트랜잭션은 앱을 실행할 때마다 StoreKit이 다시 전달합니다. 앱을 실행할 때와 상점 화면을 열 때 복구를 실행하세요. 두 시점이 겹치더라도 복구가 동시에 두 번 실행되지 않게 하고, 진행 중인 구매와 같은 주문을 동시에 처리하지 마세요.

전체 흐름

순서 구분 하는 일
1 레시피 코드 미지급 구매 조회
2 앱 코드 구매 당시 주문 정보 보완
3 레시피 코드 구매 정보 준비
4 앱 코드, 앱 서버, Hive Axyl 서버 API 영수증 검증과 상품 지급
5 레시피 코드 거래 종료
6 Hive Axyl SDK 지급 결과 기록

1. 미지급 구매 조회

  • 레시피 코드  레시피 코드를 앱에서 호출합니다.

FindUndeliveredAsync()를 호출해 완료되지 않은 트랜잭션 목록을 받으세요.

조회한 항목은 한 건씩 순서대로 처리하고, 구독 상품의 트랜잭션은 건너뛰세요. 완료되지 않은 트랜잭션 목록에는 확정하기 전에 멈춘 구독 결제의 트랜잭션도 들어 있지만, 이 복구는 소모성 상품만 처리합니다. 예제 코드의 subscriptionProductIds는 상품 ID 목록 조회 응답의 ProductSubscriptions 목록입니다. 구독 트랜잭션은 Apple App Store 구독 결제 구현에 따라 마무리하세요.

using Hive.Axyl.Samples.Recipes;

var recipe = new UndeliveredPurchaseRecipe(new ApplePurchaseSource());

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)
{
    if (subscriptionProductIds.Contains(entry.Order.ProductId))
    {
        // 구독 상품의 트랜잭션은 이 복구로 처리하지 않습니다.
        continue;
    }

    // 2단계부터 한 건씩 순서대로 처리합니다.
}

레시피가 내부에서 수행하는 작업은 아래와 같습니다.

구분 호출 확인할 곳
스토어 플러그인 GetUnfinishedTransactionsAsync()로 미완료 트랜잭션 조회 미완료 트랜잭션 조회

Apple App Store는 미지급 구매 목록을 기기에 보관합니다. 사용자가 기기를 변경했거나 앱을 다시 설치한 경우라면 StoreKit의 구매 동기화를 먼저 실행해야 목록이 채워집니다. 방법은 구매 동기화를 참조하세요.

목록이 비어 있으면 마무리할 구매가 없다는 뜻이며 오류가 아닙니다.

2. 구매 당시 주문 정보 보완

  • 앱 코드  앱에서 직접 구현합니다.

StoreKit 트랜잭션에는 결제 금액과 통화, 구매를 시작할 때 전달한 주문 정보가 들어 있지 않습니다. 레시피가 돌려준 항목의 Order.Price는 0, Order.Currency는 빈 값, Order.IapPayload는 null이며, ServerId와 AccountUuid에는 1단계의 조회 조건에 넣은 값이 들어갑니다.

검증 전에 이 값을 구매 당시의 주문 정보로 바꿔 넣으세요. 먼저 Apple App Store 결제 구현에서 저장한 구매 정보 가운데 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을 실제 결제 금액과 대조합니다. 두 기록에서 모두 찾지 못한 항목은 완료하지 않은 채 남기고 오류로 기록하세요. 완료 처리하지 않은 트랜잭션은 다음 실행에서 StoreKit이 다시 전달합니다.

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()는 Apple App Store 결제 구현에서 구현한 앱의 메서드입니다. 복구에서는 두 번째 인자 requestType으로 구매 복구를 나타내는 2를 전달하세요. 사용 예제에서는 이 인자로 PurchaseVerificationKind.Recovery를 전달하고, 앱 서버 연동 코드가 2로 바꿉니다.

// 앱 서버에 구매 복구로 영수증 검증과 상품 지급을 요청합니다.
PurchaseDeliveryConfirmation delivery = await VerifyAndGrantOnceAsync(
    prepared.Pending,
    requestType: 2,   // 2: 구매 복구. 사용 예제의 PurchaseVerificationKind.Recovery
    token);

if (delivery == null)
{
    // 앱 서버가 지급을 확인하지 않았습니다.
    // 저장한 구매 정보를 그대로 두고 다음 항목으로 넘어갑니다.
    continue;
}

4.2. 앱 서버의 영수증 검증과 상품 지급

앱 서버는 Apple App Store 결제 구현과 같은 방법으로 영수증을 검증하고 상품을 지급하세요. 새 구매와 다른 점은 아래와 같습니다.

  1. 영수증 검증 요청의 requestType에 구매 복구를 나타내는 2를 넣습니다.
  2. 새 구매와 같은 지급 기록에서 이미 지급한 구매인지 확인합니다. requestType은 중복 지급을 막는 기준이 아닙니다.
  3. 이미 지급한 구매라면 다시 지급하지 않고 지급 완료로 응답합니다. 지급 완료로 응답해야 앱이 5단계에서 거래를 종료합니다.

2단계에서 구매 당시의 주문 정보를 채웠다면 Order.IapPayload에는 구매를 시작할 때 전달한 iapPayload가 들어 있습니다. 검증 요청에 이 값을 넣으면 응답의 hiveAxylPayload로 그대로 돌아오므로, 어느 지급 대상이나 서버에 지급해야 하는지를 이 값으로 판단하세요.

5. 거래 종료

  • 레시피 코드  레시피 코드를 앱에서 호출합니다.

앱 서버가 지급 완료로 응답하면 ClosePurchaseAsync()를 호출해 트랜잭션을 완료하세요. 완료하지 않으면 다음 복구에서 또 조회됩니다.

ClosePurchaseOutcome closed = await recipe.ClosePurchaseAsync(
    prepared.Pending, delivery, token);

if (closed.Status == ClosePurchaseStatus.Success)
{
    // 구매가 끝났으므로 저장한 구매 정보를 지웁니다.
    await pendingStore.DeleteAsync(prepared.Pending);
}

레시피가 내부에서 수행하는 작업은 아래와 같습니다.

구분 호출 확인할 곳
Hive Axyl SDK RequestPurchaseAsync()로 결제 확정 결제 확정 요청
스토어 플러그인 FinishTransactionAsync()로 StoreKit 트랜잭션 완료 거래 완료 처리

6. 지급 결과 기록

지급 결과를 Hive Axyl 서버에 기록하세요. 새 구매를 처리할 때와 같습니다.

AxylTransactionId에는 4.1의 VerifyAndGrantOnceAsync()가 앱 서버 응답에서 따로 보관한 hiveAxylTransactionId를 넣으세요. 이 값은 PurchaseDeliveryConfirmation에 담기지 않습니다.

using Hive.Axyl.Payments;

var payments = HiveCore.Resolve<IPaymentsService>();

await payments.ItemResultAsync(new ItemResultBody
{
    AxylTransactionId = axylTransactionId,   // 4.1에서 앱 서버 응답으로 받아 따로 보관한 hiveAxylTransactionId
    Status = 1,   // 1: 지급 성공
});

실패 처리

복구는 여러 건을 순회하므로 한 건이 실패해도 나머지를 계속 처리하세요. 상황별로 앱이 할 일은 아래와 같습니다.

  • 조회 결과가 비어 있음: 마무리할 구매가 없는 정상 상태
  • 조회 실패: 빈 목록과 구분해야 하는 상태. 오류를 기록하고 다음 실행 때 다시 조회
  • 준비 실패: 저장한 구매 정보를 그대로 두고 다음 항목으로 진행
  • 앱 서버의 지급 미확인: 저장한 구매 정보를 그대로 두고 다음 항목으로 진행
  • 종료 실패: 검증과 지급은 이미 끝난 상태. 다음 복구에서 다시 조회되므로 그대로 두어도 됨

BusinessOutcome의 값별 대응 방법은 결제 레시피의 거절 사유를 참조하세요.

StoreKit 서명 확인 오류

레시피는 StoreKit이 트랜잭션 서명을 확인하지 못한 항목도 조회 결과에 포함하고, StoreKit이 남긴 오류를 entry.StoreVerificationError에 담아 전달합니다. 최종 판단은 앱 서버가 요청하는 Hive Axyl 서버의 영수증 검증이 내리며, 이 검증은 확인되지 않는 영수증을 거절합니다. 앱 정책으로 따로 판단하려면 상품을 지급하기 전에 이 값을 확인하세요.

다음 단계

새 구매를 구현하는 절차는 Apple App Store 결제 구현을 참조하세요.