이 오류의 의미

0:03:00

RevenueCat이 "no products found", "empty offerings", "Error fetching offerings"를 보고할 때, 그 의미는 한 가지로 정확합니다. SDK가 RevenueCat 백엔드에 도달해 offering 설정을 가져오긴 했지만, 연결 고리 어딘가에서 제품 식별자가 실제로 구매 가능한 스토어 제품으로 해석되지 못했다는 뜻입니다. 그 결과 current offering이 nil이거나 availablePackages가 0인 offering이 되고, 페이월에는 아무것도 표시되지 않습니다.

이 문제는 다른 어떤 것보다 훨씬 흔한 RevenueCat 이슈입니다. 다행인 점은, 언제나 원인이 정해져 있는 설정 문제이고 그 원인 목록이 유한하다는 것입니다. 그리고 그 모든 원인을 이 페이지에서 다룹니다. 문제를 해결하려면 getOfferings()를 호출할 때마다 SDK가 따라가는 연결 고리를 이해해야 합니다.

text
Store product (App Store / Google Play)        ← must exist, be active/approved & available
        ↓  product ID must match character-for-character
RevenueCat Product (Product Catalog → Products) ← must exist for YOUR platform
        ↓  must be attached to a package
Package (e.g. $rc_monthly)                      ← must contain a product for YOUR store
        ↓  must belong to an offering
Offering (e.g. "default")                       ← must be marked Current
        ↓  fetched via your public API key (appl_… / goog_…)
Your app                                        ← bundle ID / package name must match
        ↓  device store must be able to serve the products
The device                                      ← agreements signed, app published, tester set up

이 연결 고리 중 단 하나라도 끊기면 empty offerings가 나타납니다. 오류 메시지를 보면 보통 어느 쪽을 살펴봐야 하는지, 즉 RevenueCat 쪽인지 스토어 쪽인지 감이 잡힙니다.

오류 메시지 모음

아래는 모두 같은 계열의 문제입니다. 자신의 경우를 찾아보세요.

text
# iOS: the classic
[RevenueCat] 🍎‼️ Error fetching offerings - The operation couldn't be completed.
There's a problem with your configuration. None of the products registered in the
RevenueCat dashboard could be fetched from App Store Connect (or the StoreKit
Configuration file if one is being used).
More information: https://rev.cat/why-are-offerings-empty

# Android: platform mismatch (ConfigurationError)
[RevenueCat] 😿‼️ Error fetching offerings -
PurchasesError(code=ConfigurationError, underlyingErrorMessage=You have configured
the SDK with a Play Store API key, but there are no Play Store products registered
in the RevenueCat dashboard for your offerings. If you don't want to use the
offerings system, you can safely ignore this message. To configure offerings and
their products, follow the instructions in https://rev.cat/how-to-configure-offerings.
More information: https://rev.cat/why-are-offerings-empty

# iOS: products exist but were never submitted
WARN: ⚠️ RevenueCat SDK is configured correctly, but contains some issues…
  ⚠️ monthly (monthly): This product's status (READY_TO_SUBMIT) requires you to
     take action in App Store Connect before using it in production purchases.

# Android: store can't serve the SKU
BillingClient: Product not found - SKU is not available for purchase
BillingResponseCode: ITEM_UNAVAILABLE (4)

# Both: silent variant (no error at all)
offerings.current == nil          // or
offerings.current.availablePackages.count == 0

# Both: network family (different root cause: connectivity, not configuration)
Error: Unable to fetch offerings: NetworkError / Request timed out
참고: 이 오류로 앱이 크래시하는 일은 없습니다. 단지 페이월이 제품을 표시하지 못할 뿐입니다. 다만 원인이 서명하지 않은 계약이나 제출하지 않은 제품이라면 프로덕션까지 반드시 이어지므로, 재시도로 우회하지 말고 제대로 해결하세요.

5분 진단

0:03:00

다른 것을 확인하기 전에 먼저 디버그 로깅을 켜세요. 아래의 모든 진단은 로그에 무엇이 찍혔는지에서 출발하기 때문입니다.

text
Swift:         Purchases.logLevel = .debug          (before configure)
Kotlin:        Purchases.logLevel = LogLevel.DEBUG  (before configure)
React Native:  Purchases.setLogLevel(LOG_LEVEL.DEBUG)
Flutter:       await Purchases.setLogLevel(LogLevel.debug)

그런 다음 아래 표에서 자신의 증상을 찾으세요. 각 행은 검증된 해결책이 있는 섹션으로 연결됩니다.

증상가장 유력한 원인이동
로그에 ConfigurationError… Play Store API key, but there are no Play Store products registered(또는 App Store 버전)가 찍힘offering에 반대편 스토어의 제품만 들어 있음대시보드 해결, 원인 D
offerings.currentnil이고, 로그에 스토어 오류 없음Current로 지정된 offering이 없거나, 그 안에 package/제품이 없음대시보드 해결, 원인 A~C
iOS, 연동 최초인데 "None of the products registered… could be fetched"Paid Applications Agreement가 Active가 아니거나, StoreKit Configuration 파일 없는 시뮬레이터iOS 해결, 원인 1 & 6
로그에 READY_TO_SUBMIT 경고 / 제품이 "Missing Metadata"로 표시됨제품이 미완성이거나 빌드와 함께 제출된 적 없음iOS 해결, 원인 2 & 3
RevenueCat REST API / 대시보드는 전부 정상인데, iOS에서 availablePackages가 0바이너리를 한 번도 업로드하지 않아 StoreKit이 제품 ID를 활성화하지 못함iOS 해결, 원인 4
본인은 되는데 다른 나라 사용자는 실패함제품 판매 범위가 특정 국가/지역으로 제한됨iOS 해결, 원인 7 / Android 해결, 원인 6
Android: 실제 기기에서 ITEM_UNAVAILABLE 또는 empty offerings앱이 테스트 트랙에 없거나, 테스터가 opt-in하지 않았거나, 설치한 빌드가 Play에서 온 것이 아님Android 해결, 원인 1~3
Android: 대시보드에 자격 증명 경고가 뜨거나, 설정한 지 36시간이 안 됨Play 서비스 자격 증명이 잘못되었거나 아직 전파 중Android 해결, 원인 5
Expo Go의 React Native: 네이티브 모듈 없음 / 제품이 빈 상태Expo Go는 네이티브 모듈을 실행할 수 없어서 dev build가 필요함크로스 플랫폼
getProducts()는 되는데 getOfferings()가 비어 있음끊긴 지점이 대시보드의 제품과 offering 사이에 있음대시보드 해결
어제는 됐는데 오늘은 비어 있고, 바꾼 게 없음스토어/캐시 전파 문제이거나, 누군가 offering을 수정함아직도 막혔나요?
NetworkError / 타임아웃설정 문제가 아니라 연결, VPN/방화벽, 또는 (드물게) 서비스 장애아직도 막혔나요?
핵심 진단법: 디버그 로그에는 RevenueCat이 스토어에 요청한 제품과 스토어가 돌려준 결과가 찍힙니다. 로그에 제품 ID를 요청한 기록은 있는데 반환된 제품이 0이라면, 문제는 스토어 쪽입니다(04~05단계). 로그에 제품 ID를 요청하는 기록조차 없다면, 문제는 RevenueCat 대시보드 쪽입니다(03단계).

RevenueCat 대시보드 해결

0:04:00

RevenueCat 대시보드에서 아래 항목을 순서대로 점검하세요. 대시보드 쪽에서 연결 고리가 끊기는 모든 경우를 다룹니다.

A. offering 하나는 반드시 "Current"여야 합니다

getOfferings()current offering을 current 속성에 담아 반환합니다. Product Catalog → Offerings로 이동해 정확히 하나의 offering에 Current 배지가 붙어 있는지 확인하세요. 그렇지 않다면 해당 offering의 메뉴를 열고 Make Current를 선택합니다. 이 설정이 없으면 다른 모든 것이 완벽해도 offerings.currentnil이 됩니다.

B. current offering에는 반드시 package가 있어야 합니다

current offering을 여세요. Packages 섹션에는 최소 하나의 package(예: $rc_monthly, $rc_annual)가 나열되어 있어야 합니다. package가 0인 offering은 오류 없이 빈 페이월을 반환합니다.

C. 모든 package에는 각 플랫폼용 제품이 필요합니다

각 package를 열어 붙어 있는 제품을 확인하세요. 하나의 package에는 App Store 제품 Google Play 제품을 함께 담을 수 있으며, 출시하는 모든 플랫폼에 대해 각각 하나씩 필요합니다. iOS 제품만 있는 package는 iOS에서 테스트할 때는 멀쩡해 보이지만 Android에서는 조용히 깨집니다.

D. ConfigurationError 플랫폼 불일치

아래 오류의 정확한 원인이며, 순전히 대시보드 설정 문제입니다.

text
PurchasesError(code=ConfigurationError, underlyingErrorMessage=You have configured
the SDK with a Play Store API key, but there are no Play Store products registered
in the RevenueCat dashboard for your offerings. …)

풀어 보면, 앱이 goog_ API 키로 실행 중인데 current offering의 package에 들어 있는 제품이 전부 App Store 소속이라는 뜻입니다(반대로 appl_ 키에 Play 전용 제품만 있는 경우도 마찬가지). SDK가 통신하는 스토어에는 가져올 것이 없는 셈입니다. 해결 방법은 다음과 같습니다.

  1. Product Catalog → Products에서 + New를 클릭해 빠져 있는 스토어의 제품을 만듭니다(Google Play 제품 ID, 예를 들어 base plan이 있는 구독이라면 premium_monthly:monthly-autorenew). 또는 자동으로 가져올 수도 있습니다.
  2. current offering을 열고 → 각 package → Edit → 기존 제품 옆에 새 스토어의 제품을 붙입니다.
  3. 앱을 다시 실행하세요. 모든 package에 활성 스토어용 제품이 갖춰지면 오류가 사라집니다.

E. 제품 ID는 스토어와 문자 하나까지 일치해야 합니다

Product Catalog → Products에서 각 식별자를 App Store Connect / Google Play Console과 비교하세요. 대소문자와 공백까지 구분됩니다. com.app.monthlycom.app.Monthly이며, 붙여넣을 때 딸려 오기 쉬운 끝의 공백 하나가 조용히 문제를 일으킵니다. 확실하지 않다면 ID를 지우고 조심스럽게 다시 붙여넣으세요.

F. 올바른 프로젝트의 올바른 API 키

  • Project Settings → API Keys에서 플랫폼별 공개(public) 키를 사용하세요. App Store는 appl_…, Play는 goog_…입니다. 앱에 비밀 키인 sk_…는 절대 쓰지 마세요.
  • RevenueCat 프로젝트가 여러 개라면(예: 스테이징/프로덕션) 지금 편집 중인 대시보드의 프로젝트에 그 키가 속하는지 확인하세요. 다른 프로젝트의 키는 그 프로젝트의 (비어 있는) offering을 가져옵니다. "설정은 다 했는데!" 하는 전형적인 함정입니다.
  • Apps & providers의 bundle ID(iOS) / package name(Android)은 빌드가 실제로 사용하는 값과 일치해야 하며, .debug나 flavor 접미사(예: com.app.dev)까지 포함해야 합니다. 접미사가 붙은 디버그 빌드는 com.app으로 설정된 앱과 일치하지 않습니다.
체크리스트 요약: 스토어 제품 존재 → RevenueCat에 동일한 제품 ID → 해당 플랫폼용 제품이 package에 붙음 → package가 offering 안에 있음 → offering이 Current로 지정됨 → 올바른 공개 API 키 + 일치하는 bundle ID/package name.

iOS / App Store 해결

0:05:00

해결된 커뮤니티 스레드를 기준으로, 실제 원인으로 밝혀지는 빈도가 높은 순서로 정리했습니다.

1. Paid Applications Agreement (가장 흔히 숨어 있는 원인)

이 계약이 Active가 되기 전까지, Apple은 샌드박스를 포함해 앱에 인앱 구매 제품을 전혀 제공하지 않습니다. 게다가 이 계약은 조용히 만료되기도 하는데, 그래서 "몇 달 동안 잘 되던" 앱이 갑자기 빈 페이월을 보이곤 합니다. 여러 커뮤니티 스레드에서 검증된 해결책은 다음과 같습니다.

  1. App Store ConnectBusiness(예전 명칭: Agreements, Tax, and Banking)로 이동합니다.
  2. Paid Applications AgreementActive로 표시되어야 합니다. "Pending"도, "New"도, 만료 상태도 아니어야 합니다. 메시지가 뜨면 최신 약관에 동의하세요.
  3. Banking InformationTax Forms(미국은 W-9, 미국 외는 W-8BEN)를 완료하세요. 둘 다 끝나기 전까지는 계약이 Active가 되지 않습니다.
  4. 완료 후 Apple의 검증에 최대 24~48시간을 기다립니다.

2. 제품 상태: 메타데이터를 완성하세요

App Store Connect → 앱 → Subscriptions / In-App Purchases에서 각 제품이 Missing MetadataDeveloper Action Needed 상태여서는 안 됩니다. 제품을 가져올 수 있는 상태로 만들려면 최소 한 지역의 가격, 최소 하나의 현지화(localization)(표시 이름 + 설명), 그리고 심사용 스크린샷이 필요합니다. 샌드박스/StoreKit 테스트에는 Ready to Submit이면 충분하고, 프로덕션에는 Approved가 필요합니다.

3. 구독은 반드시 Subscription Group에 속해야 합니다

Apple은 모든 자동 갱신 구독이 subscription group에 속하도록 요구합니다. API로 만들었거나 급하게 만든 제품이라면 각각 그룹에 배정되어 있는지 확인하세요. 그룹에 속하지 않은 구독은 해석되지 않습니다. (널리 공유된 어느 커뮤니티 글에서 검증된 해결책입니다.)

4. 바이너리를 한 번은 업로드하세요 (StoreKit 제품 활성화)

갓 만든 앱이 흔히 빠지는 함정이 있습니다. 모든 설정이 올바르고 RevenueCat REST API도 offering을 반환하는데, 기기에서는 availablePackages가 0인 경우입니다. Apple은 새 제품 ID를 앱에 연결하려면 최소 한 번 업로드된 바이너리를 처리해야 합니다. 아무 빌드나 아카이브해서 App Store Connect에 업로드하고(TestFlight로 충분하며 심사는 필요 없습니다) 처리가 끝날 때까지(15분에서 몇 시간) 기다리면 제품이 해석되기 시작합니다.

5. Bundle ID와 capability

  • Xcode → 타겟 → Signing & Capabilities: Bundle Identifier가 App Store Connect RevenueCat → Apps & providers와 정확히 일치해야 합니다.
  • 타겟에 In-App Purchase capability가 없다면 추가하세요.

6. 시뮬레이터에는 StoreKit Configuration 파일이 필요합니다

iOS 시뮬레이터는 App Store Connect와 통신하지 않습니다. scheme에 StoreKit Configuration 파일을 선택하지 않으면 제품 요청이 빈 결과로 돌아옵니다. 첫날에 가장 흔히 겪는 경험입니다. 두 가지 방법 중 하나를 쓰세요.

  1. File → New → File → StoreKit Configuration File로 파일을 만들고 정확히 같은 제품 ID로 제품을 추가한 다음, Product → Scheme → Edit Scheme → Run → Options → StoreKit Configuration에서 그 파일을 선택합니다. 또는
  2. 샌드박스 테스터(Settings → App Store → Sandbox Account)를 사용해 실제 기기에서 테스트하고, StoreKit Configuration은 None으로 둡니다.
반대 방향의 함정: 실제 App Store/샌드박스 제품을 기대하는데 StoreKit Configuration 파일을 켠 채로 두거나(또는 App Store Connect와 어긋난 제품 ID를 쓰는 경우) 마찬가지로 empty offerings가 발생합니다. 샌드박스를 대상으로 테스트할 때는 StoreKit Configuration을 다시 None으로 설정하세요.

7. 국가 / 지역 판매 범위

검증된 커뮤니티 해결책 하나를 소개합니다. 구독이 한 국가에서만 판매 가능했던 탓에, 다른 곳의 테스터들은 empty offerings를 받았습니다. App Store Connect에서 각 제품의 Availability를 확인하고, 본인과 테스터가 있는 모든 국가를 포함하는지 확인하세요. 샌드박스 계정에도 국가가 지정되어 있다는 점을 잊지 마세요.

8. 전파 시간

새 제품과 메타데이터 수정은 Apple 시스템 전반에 전파되기까지 몇 시간에서 약 24시간이 걸릴 수 있습니다. 방금 제품을 만들었고 위의 모든 체크리스트 항목을 통과했다면, 다른 것을 바꾸기 전에 기다려 보세요.

App 심사 참고: Apple의 심사 환경은 제품을 가져오는 데 악명 높을 만큼 불안정합니다. offering 가져오기가 실패해도 화면이 깨지지 않게 처리하고(빈 화면 대신 재시도 버튼을 두고), 심사자가 "제품 없음"을 보고하면 위 항목이 하나도 되돌아가지 않았는지 확인한 뒤 재제출하세요.

Android / Google Play 해결

0:05:00

iOS와 달리 Android에는 오프라인 StoreKit 같은 것이 없습니다. Billing Library는 항상 살아 있는 Google Play 백엔드와 통신하므로, Play가 앱과 제품, 그리고 테스터를 모두 알고 있어야 합니다. 빈도가 높은 순서로 정리했습니다.

1. 앱이 반드시 테스트 트랙에 게시되어야 합니다

Google Play는 한 번도 본 적 없는 앱에는 제품을 반환하지 않습니다. 서명된 릴리스 빌드(AAB)를 최소한 Internal testing 트랙(Play Console → Release → Testing → Internal testing)에 업로드하고, 트랙이 테스터에게 사용 가능 상태로 표시되는지 확인하세요. Internal testing에는 심사가 필요 없습니다.

2. 테스터를 추가하고 opt-in까지 해야 합니다

모두가 놓치는 단계입니다. Google 계정을 테스터 목록에 추가하는 것만으로는 부족합니다.

  1. 본인의 Google 계정을 트랙의 테스터로 추가합니다.
  2. opt-in 링크(Play Console의 "How testers join your test" 아래에 표시됨)를 그 계정으로 로그인한 기기에서 열고 Become a tester를 탭합니다.
  3. 그 링크를 통해 Google Play에서 앱을 최소 한 번 설치합니다. 그 뒤로는 같은 applicationId와 서명 설정을 가진, 로컬에서 빌드한 디버그 빌드도 제품을 가져올 수 있습니다.

3. 기기의 빌드가 Play의 기대와 일치해야 합니다

  • build.gradleapplicationId가 Play Console 및 RevenueCat → Apps & providers와 정확히 일치해야 합니다. build type이 붙이는 .debug 접미사를 주의하세요.
  • 기기의 기본 Google Play 계정이 테스터 계정이어야 합니다. 여러 계정이 있는 기기는 엉뚱한 계정으로 Play에 조회하는 경우가 많습니다. 확실하지 않다면 Play Store 데이터를 지우거나, 테스터 계정만 있는 프로필을 사용하세요.
  • 에뮬레이터는 (단순 "Google APIs"가 아니라) Google Play Store 이미지를 포함해야 하고, 테스터로 로그인되어 있어야 합니다.

4. 제품이 Active여야 합니다 (일회성 제품은 활성화 필요)

Play Console → Monetize → Products에서 offering이 참조하는 모든 구독과 인앱 제품이 Active로 표시되어야 합니다. 새로 만든 인앱 제품은 Activate를 누르기 전까지 draft 상태입니다. 구독의 경우 base plan도 활성 상태인지 확인하세요. active한 base plan이 없는 구독은 ITEM_UNAVAILABLE을 반환합니다.

5. 서비스 자격 증명: 유효하고 전파 완료 (최대 36시간)

RevenueCat이 구매를 검증하고 (가져오기 기능을 위해) 제품을 읽으려면 유효한 Play 서비스 자격 증명이 필요합니다. RevenueCat → Apps & providers → Google Play에서 자격 증명 검사를 통과해야 합니다. 검증된 함정이 두 가지 있습니다.

  • 갓 만든 서비스 자격 증명은 Google 시스템 전반에 전파되기까지 최대 36시간이 걸릴 수 있습니다. 오늘 Play 설정을 끝냈고 다른 모든 항목이 정상이라면, 이게 답일 가능성이 높습니다. 기다려 보세요.
  • 전파를 재촉하는 간단한 방법: Play Console에서 아무 제품의 설명을 편집하고 저장하면 Google의 캐시를 자극할 수 있습니다.

6. 국가별 판매 범위

앱의 country availability(Play Console → Release → Production → Countries/regions)와 각 구독의 지역별 가격을 확인하세요. 앱이나 제품이 판매되지 않는 국가의 테스터는 빈 결과를 받습니다.

7. 여전히 ITEM_UNAVAILABLE인가요?

이 응답 코드는 "Play가 앱은 알지만 이 사용자에게 이 SKU를 판매하지 않겠다"는 뜻입니다. 원인 1~3을 이 관점으로 다시 짚어 보세요. 올바른 트랙인가? 올바른 계정인가? Play에서 최소 한 번 설치했는가? 거의 항상 이 셋 중 하나로 귀결됩니다.

기다리는 동안 순서를 건너뛰세요: RevenueCat의 Android용 Test Store를 사용하면 Play Console 설정 없이 전체 구매 흐름을 만들고 테스트할 수 있습니다. 트랙이 게시되고 자격 증명이 전파되는 동안 유용합니다.

React Native, Flutter & 크로스 플랫폼

0:03:00

03~05단계의 모든 내용은 React Native, Flutter, KMP에도 그대로 적용됩니다. SDK는 결국 동일한 네이티브 StoreKit / Play Billing API를 호출하기 때문입니다. 다음 원인들은 크로스 플랫폼 설정에 특유한 것들입니다.

Expo Go는 제품을 가져올 수 없습니다, 절대로

react-native-purchases는 네이티브 모듈이며, Expo Go에는 포함되어 있지 않습니다. 그곳에서는 offering/제품이 항상 빈 상태로 돌아옵니다. EAS Build로 development build를 만들어(npx expo run:ios / eas build --profile development) 거기서 테스트하세요. 네이티브 모듈을 제거하는 모든 환경에서도 마찬가지입니다.

가져오기 전에 configure하세요 (레이스 컨디션)

Purchases.configure()가 완료되기 전에 getOfferings()를 호출하면(JS 모듈 경계를 넘거나 병렬 useEffect에서 쉽게 벌어집니다) 오류나 빈 결과가 반환됩니다. configure는 가능한 한 이르게 한 번만 하고(예: 앱 루트에서), API가 허용하는 곳이라면 offering을 가져오기 전에 await하세요.

각 플랫폼에서 플랫폼에 맞는 키를 쓰세요

크로스 플랫폼 앱은 런타임에 선택되는 키가 모두 필요합니다.

typescript
// React Native
import { Platform } from 'react-native';
import Purchases, { LOG_LEVEL } from 'react-native-purchases';

Purchases.setLogLevel(LOG_LEVEL.DEBUG);
Purchases.configure({
  apiKey: Platform.OS === 'ios' ? 'appl_YOUR_IOS_KEY' : 'goog_YOUR_ANDROID_KEY',
});

Android에 iOS 키를 넘기면(또는 그 반대) 바로 03단계에서 다룬 ConfigurationError가 발생합니다.

getProducts() vs getOfferings() 진단법

여러 React Native 스레드에서 보고된 패턴이 있습니다. Purchases.getProducts([...])는 제품을 반환하는데 getOfferings()는 비어 있는 경우입니다. 사실 이건 좋은 소식입니다. 스토어 쪽은 동작한다는 증거이고, 끊긴 지점을 대시보드 연결 고리(package → offering → current)로 좁혀 주기 때문입니다. 03단계로 돌아가 원인 A~D를 확인하세요. iOS에서는 Paid Applications Agreement도 확인하세요. 일부 설정에서는 getProducts가 부분적으로 동작하는 동안에도 이 계약 때문에 offerings가 깨질 수 있습니다.

Flutter 참고: purchases_flutter에서 대응하는 호출은 Purchases.getOfferings() / Purchases.getProducts()이며, 같은 진단법이 적용됩니다. hot reload는 configure()를 다시 실행하지 않습니다. 키를 바꾼 뒤에는 완전히 재시작하세요.

코드로 디버깅하기

0:04:00

아래 스니펫은 연결 고리가 정확히 어디서 끊기는지 출력합니다. offerings 객체 → current offering → package → 제품 순서입니다. 하나를 실행해 출력을 읽으면, 이 가이드에서 봐야 할 섹션을 가리켜 줍니다.

swift
// Swift: walk the chain
Purchases.logLevel = .debug  // before configure

Purchases.shared.getOfferings { offerings, error in
    if let error = error {
        print("❌ Offerings error: \(error.localizedDescription)")
        // ConfigurationError → step 03 (platform mismatch / dashboard)
        // Network errors    → step 08
        return
    }
    guard let offerings = offerings else {
        print("❌ Offerings object is nil → check API key & project (step 03-F)")
        return
    }
    print("All offerings: \(offerings.all.keys.joined(separator: ", "))")

    guard let current = offerings.current else {
        print("⚠️ No current offering → mark one Current (step 03-A)")
        return
    }
    print("Current offering: \(current.identifier)")

    if current.availablePackages.isEmpty {
        print("⚠️ 0 packages resolved → store could not serve the products (steps 04-05)")
        print("   (If the dashboard shows packages, this is store-side: agreement,")
        print("    product status, binary activation, testing track, or tester setup.)")
    } else {
        for package in current.availablePackages {
            print("✅ \(package.identifier) → \(package.storeProduct.productIdentifier) @ \(package.storeProduct.localizedPriceString)")
        }
    }
}
kotlin
// Kotlin: walk the chain
Purchases.logLevel = LogLevel.DEBUG  // before configure

Purchases.sharedInstance.getOfferingsWith(
    onError = { error ->
        Log.e("RC", "❌ Offerings error: ${error.message} (code: ${error.code})")
        // ConfigurationError → step 03 (platform mismatch / dashboard)
    },
    onSuccess = { offerings ->
        Log.d("RC", "All offerings: ${offerings.all.keys.joinToString()}")

        val current = offerings.current
        if (current == null) {
            Log.w("RC", "⚠️ No current offering → mark one Current (step 03-A)")
            return@getOfferingsWith
        }
        Log.d("RC", "Current offering: ${current.identifier}")

        if (current.availablePackages.isEmpty()) {
            Log.w("RC", "⚠️ 0 packages resolved → Play could not serve the products (step 05)")
            Log.w("RC", "   Check: testing track, tester opt-in, install-from-Play, Active products")
        } else {
            current.availablePackages.forEach { pkg ->
                Log.d("RC", "✅ ${pkg.identifier} → ${pkg.product.id} @ ${pkg.product.price.formatted}")
            }
        }
    }
)
typescript
// React Native: walk the chain (same logic for Flutter's purchases_flutter)
import Purchases, { LOG_LEVEL } from 'react-native-purchases';

Purchases.setLogLevel(LOG_LEVEL.DEBUG);

try {
  const offerings = await Purchases.getOfferings();
  console.log('All offerings:', Object.keys(offerings.all));

  if (!offerings.current) {
    console.warn('⚠️ No current offering → mark one Current (step 03-A)');
  } else if (offerings.current.availablePackages.length === 0) {
    console.warn('⚠️ 0 packages resolved → store-side issue (steps 04-05)');
  } else {
    offerings.current.availablePackages.forEach((pkg) =>
      console.log(`✅ ${pkg.identifier} → ${pkg.product.identifier} @ ${pkg.product.priceString}`),
    );
  }
} catch (e) {
  console.error('❌ Offerings error:', e);
  // ConfigurationError → step 03; native module missing in Expo Go → step 06
}

디버그 로그 읽기

  • Configuring Purchases with API key: appl_/goog_…: 기대한 키와 플랫폼이 맞는지 확인하세요.
  • Requesting products from the store with identifiers: …: 대시보드 연결 고리는 정상이고, 이제 문제는 스토어가 무엇을 돌려주느냐입니다.
  • Fetched 0 products from the store / 유효하지 않은 식별자가 나열됨: 스토어 쪽 원인이므로 04단계(iOS) 또는 05단계(Android)로 이동하세요.
  • 제품 요청이 아예 로그에 없음: 대시보드 쪽 원인이므로 03단계로 이동하세요.

아직도 막혔나요? + 관련 가이드

0:02:00

격리 도구: 어느 계층이 깨졌는지 밝혀내기

  • 전파를 기다리기: 새 제품(Apple은 최대 약 24시간), 새 Play 자격 증명(최대 36시간), 갓 서명한 계약은 모두 시간이 걸립니다. 오늘 설정했다면, 가장 효과적인 해결책이 그저 하루 기다리는 것일 때가 많습니다.
  • 네트워크 계열 오류(NetworkError, 타임아웃): 연결을 확인하고, VPN/프록시를 끄고, api.revenuecat.com이 방화벽에 막히지 않았는지 확인하고, 페이월 로드에 backoff를 적용한 재시도를 추가하세요. 특히 App Store 심사 중에는 페이월이 깨지지 않고 자연스럽게 대체 화면을 보여 주도록 설계하세요.
  • 앱 없이 대시보드 연결 고리 테스트하기: REST API를 호출하세요. 이 호출이 제품이 포함된 offering을 반환한다면 대시보드는 정상이며, 문제는 스토어 쪽이거나 앱에 있습니다.
    bash
    curl -s 'https://api.revenuecat.com/v1/subscribers/test_user/offerings' \
      -H 'Authorization: Bearer YOUR_PUBLIC_API_KEY' \
      -H 'X-Platform: ios'   # or android
  • 코드 없이 설정 테스트하기: RevenueCat의 SampleCat 샘플 앱에 여러분의 API 키를 넣어 실행하세요. SampleCat은 offering을 가져오는데 여러분의 앱은 못 가져온다면, 차이는 여러분의 앱에 있습니다(초기화 순서, 키, StoreKit 설정, bundle ID).
  • 스토어 없이 코드 테스트하기: RevenueCat의 Test Store (iOS) / Test Store (Android)는 Apple/Google을 완전히 우회합니다. Test Store 키로 offering이 로드된다면 여러분의 코드와 대시보드는 정상이므로, 문제는 스토어 쪽 설정입니다.
  • 대시보드 수정 후 캐시가 오래된 경우: offering은 약 5분 동안 캐시됩니다. 대시보드를 변경한 뒤에는 앱을 강제 종료하고 다시 실행하세요. (Android에서는 Play Store 앱 자체의 캐시가 뒤처질 수 있는데, Play Store 데이터를 지우면 도움이 됩니다.)

완전 체크리스트

  • ☐ offering 하나가 Current로 지정되어 있고, package가 있으며, 각 package에 해당 플랫폼용 제품이 담겨 있음
  • ☐ 제품 ID가 스토어와 동일함(대소문자, 공백)
  • ☐ 올바른 프로젝트의 올바른 공개 API 키(appl_/goog_); bundle ID / applicationId 일치
  • ☐ iOS: Paid Applications Agreement가 Active(뱅킹 + 세금 완료, 만료 아님)
  • ☐ iOS: 제품에 가격, 현지화, 심사 스크린샷이 있음; 구독이 subscription group에 속함
  • ☐ iOS: 바이너리를 최소 하나 업로드 & 처리함; 시뮬레이터는 StoreKit Configuration 파일 사용(또는 기기 + 샌드박스 계정에서 None으로 설정)
  • ☐ Android: 서명된 빌드가 internal testing에 있음; 테스터 추가 및 opt-in 완료; Play를 통해 한 번 설치함
  • ☐ Android: 제품(및 base plan)이 Active; Play 자격 증명이 유효하고 설정한 지 36시간 이상 지남
  • ☐ 공통: 제품/앱 판매 범위가 자신의 국가를 포함함; 디버그 로그를 처음부터 끝까지 확인함

모든 항목을 체크했는데도 offerings가 여전히 비어 있다면, 디버그 로그를 모아 RevenueCat Community에 질문하거나 지원팀에 문의하세요. 이때 Error fetching offerings 주변의 로그 줄을 함께 포함하세요.

관련 가이드