스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
선제적인 앱 내 구입 복원 구현
사용자가 앱을 처음 열 때 앱 내 구입 액세스를 사전에 복원하는 방법을 알아보세요. StoreKit 또는 StoreKit 2를 사용하여 기존 구독에 즉시 액세스할 수 있도록 하는 방법을 보여드리고, 클라이언트와 서버 구현 모두에 대한 모범 사례를 다루겠습니다. 고객의 구입 상태를 결정하고 앱에 개인화된 온보딩 경험을 만드는 방법에 대해 자세히 알아볼 수 있습니다.
리소스
- App Store Server Notifications
- CloudKit
- Determining service entitlement on the server
- Implementing a store in your app using the StoreKit API
- Introducing StoreKit 2
- Reducing Involuntary Subscriber Churn
관련 비디오
WWDC22
WWDC21
Tech Talks
WWDC20
-
다운로드
안녕하세요 저는 David Wendland입니다 저는 App Store의 상거래 기술담당자입니다 오늘은 여러분의 앱에서 고객이 어떤 조치를 취하지 않더라도 미래, 현재, 과거 구매를 선제적으로 확인하여 고객들에게 최고의 경험을 제공하는 방법을 설명드리겠습니다 StoreKit 2와 StoreKit을 사용하겠습니다 고객들의 앱에서의 경험을 최적화할 수 있도록요 그럼 먼저 선제적 앱 내 구입 복원이란 무엇일까요? 고객이 앱을 시작할 때 기기에서 사용 가능한 데이터를 미리 준비하는 겁니다 거래 내역을 미리 확인하는 거죠 신규 고객인지 기존 고객인지 확인하기 위해서요 이렇게 하면 고객에게 어떤 행동도 요청할 필요 없습니다 구매 복원 버튼을 누르거나 비밀번호 입력을 할 필요도 없어요 이를 통해 고객의 구매 내역 및 상태 관련 앱 경험을 맞춤화할 수 있습니다 앱은 현재 고객에게는 제품이나 서비스를 보여줄 수 있고요 신규 고객에게는 최신 제품을 권할 수도 있죠 과거 구독자에게는 구독 프로모션을 제시하여 고객을 다시 되찾을 수도 있겠죠 선제적 복원은 이런 일을 합니다 StoreKit을 사용하여 앱 경험을 최적화하는 거죠 신규, 기존, 과거 고객에 대해 모든 기기에서, 자동으로요 예를 살펴봅시다 바다 일기 앱이 있습니다 이것은 일반적인 상품 판매 앱입니다 고객이 선택할 수 있는 몇 가지 옵션이 제시됩니다 앱 내 구입을 할 수도 있고요 FaceID와 같은 생체 인식으로 인증할 수도 있고요 또는 앱 계정이 있다면 로그인하고 키체인을 사용하여 비밀번호를 입력할 수 있습니다 또는 활성 구독자라면 '구매 복원' 버튼을 사용할 수도 있습니다 여러분이 활성 구독자인데 새로운 기기를 사용하고 있다면 어떤 옵션을 선택해야 하는지 항상 분명히 알고 있진 않겠죠 앱에서 쉽게 사용할 수 있는 데이터를 활용하여 간소화할 수 있습니다 선제적 앱 내 구입 복원의 모범 사례를 보여드리겠습니다 활성 구독자가 새 기기에서 앱을 시작한 경우 앱이 시작될 때 서비스를 자동으로 선제적으로 복원합니다 어떤 조치도 요구하지 않고요 여기 보시면 앱이 저의 Pro 구독을 인식했습니다 제가 가장 좋아하는 해변을 로드했습니다, 서핑 조건과 라이브 캠 기능을 활성화해서 보여줍니다 이런 경험은 여러분의 앱을 다른 앱들과 차별화할 수 있습니다 어떻게 하는지 봅시다, iOS 15 이상에서 StoreKit 2를 사용합니다 또한 앱이 이전 버전의 iOS를 지원한다면 그 경우에도 이런 훌륭한 경험을 만드는 방법을 알려 드리겠습니다 StoreKit, verifyReceipt 엔드 포인트를 사용하면 됩니다 이런 배경 지식을 가지고 여기에서 다룰 내용을 알아봅시다 먼저 여러분의 앱에서 사용자 경험을 개인화하는 데 필요한 주요 고객 제품 상태를 자세히 설명하겠습니다 StoreKit을 통한 고객의 앱 내 구입를 기반으로 하죠 그런 다음 StoreKit 2를 사용하여 구현하는 단계를 살펴보겠습니다 SK Demo 앱을 사용하여 샘플 코드로 완성해 보겠습니다 각 앱 내 구입 유형 및 주요 고객 제품 상태를 살펴보고
앱 경험 개인화 사례 몇 개를 검토하겠습니다 시작해 봅시다 선제적 복원에 적용되는 앱 내 구입 유형은 비소모품, 비갱신 구독 자동 갱신 구독입니다 이것들은 모두 고객의 거래 내역에 영구적으로 남아 있기 때문에 항상 StoreKit에서 사용할 수 있습니다 따라서 앱은 고객별로 각 제품 또는 구독 그룹에 대한 구매 상태를 식별할 수 있습니다 고객 제품 상태를 검토할 때 비갱신 및 자동 갱신 구독은 모두 '구독'이라는 용어로 지칭하겠습니다 이것들은 앱이 개인화할 수 있는 세 가지 주요 상태입니다 그럼 신규 고객에 대해 자세히 살펴봅시다 이 상태는 현재 또는 과거의 앱 내 구입 거래가 없는 로그인된 App Store Apple ID를 뜻합니다 이 상태는 일반적으로 앱의 기본 상품 환경입니다 바다 일기 앱은 월간, 연간 구독에 1개월 무료 평가판을 제공하고 있습니다 두 번째 주요 상태로는 구매한 구독자와 활성 구독자가 있습니다 이 상태의 고객은 활성 거래가 있는 고객입니다 여러분의 앱은 고객에게 구매한 제품 또는 서비스에 대한 액세스 권한을 부여할 의무가 있습니다 바다 일기 앱은 즉시 고객이 선호하는 해변의 프리미엄 라이브 해변 캠을 제공합니다 서비스가 선제적으로 복원되었으니 구매 버튼은 표시되지 않습니다 구매한 각 제품 또는 활성 구독의 트랜잭션에는 정적 고유 트랜잭션 ID가 있습니다 고객의 Storefront Apple ID에서도 유지됩니다 고객의 거래 상태를 유지하기 위해 원래 트랜잭션 ID를 여러분의 시스템의 계정과 연결합니다 익명 계정일 수도 있고 또는 사용자가 시스템에서 만든 계정일 수도 있습니다 원래 트랜잭션 ID는 필수적으로 알고 있어야 합니다 그래야 App Store 서버 알림 기능을 활용하거나 서버가 현재 트랜잭션 상태를 유지하도록 할 수 있습니다 제가 강조하고 싶은 시나리오는 고객의 구독이 자동 갱신에 실패한 경우입니다 그래서 청구 재시도라는 상태로 바뀌는 경우입니다 그러면 최대 60일 동안 구독을 복구하려고 시도합니다 App Store Connect에서 결제 유예 기간 기능을 선택한 경우 유예 기간에 결제를 다시 시도하는 가입자들은 여러분이 그들의 구독을 복구하려고 시도하는 동안 계속해서 구독 서비스에 액세스할 수 있습니다 그 고객들은 여전히 여러분의 서비스에 액세스할 수 있지만 지불 문제를 해결하기 위해 간단한 클릭 유도 문구를 보여줘야 합니다 결제 재시도 및 결제 유예 기간에 대해 자세히 알아보려면 비자발적 가입자 손실 줄이기에 대한 세션 링크와 리소스를 확인하세요 마지막 주요 상태는 비활성 구매 또는 비활성 가입자입니다 이 상태는 이전에 앱 내 구입를 했으나 만료 또는 취소되어서 더 이상 해당 제품이나 서비스에 대한 구독 자격이 없는 고객입니다 이러한 트랜잭션은 지속적이며 원래 트랜잭션 ID를 갖고 있습니다 따라서 여러분은 기기나 플랫폼에 상관없이 상태를 유지할 수 있죠 구독의 경우 비활성 상태는 만료 날짜에 따라 결정됩니다 모든 앱 내 구입 유형에 대해 해지 날짜가 있으면 비활성될 수 있습니다 이 일은 거래가 환불되거나 가족 공유를 통해 부여된 액세스 권한이 취소된 경우 일어납니다 만료 또는 취소로 비활성된 구독자를 되찾기 위해 구독 프로모션을 제시해 보세요 결제 재시도 상태에 있는 사용자에게도 결제 문제를 해결하기 위해 동일한 클릭 유도 문구을 제시하는 것을 잊지 마세요 정리하면 이 세 가지가 앱에서 사용할 주요 고객 제품 상태입니다 앱 내 구입를 선제적으로 복원하고 앱 경험을 고객에 맞게 조정하기 위해 사용됩니다 바다 일기 앱으로 세 가지 상태의 앱 경험을 나란히 살펴봅시다
신규 고객은 최신 제품 및 신규 구독 프로모션을 볼 수 있습니다 현재 활성 고객은 앱이 잘 작동한다고 느낄 겁니다 고객들의 모든 기기에서 제품, 서비스에 대한 액세스를 간소화했으니까요 비활성 구독자에게는 프로모션 코드 또는 프로모션 특가 같은 최신 구독 혜택을 제공할 수 있습니다 이렇게 세 가지 주요 고객 제품 상태를 살펴보았어요 이렇게 상태를 지원하면 고객을 얻는 데 큰 도움이 됩니다 그러나 물론 더 많은 경험을 제공할 기회도 있습니다 여러분의 앱은 고객 경험을 확장하거나 개선할 수 있습니다 여러분의 제품 제안 및 비즈니스 모델 정책, 우선 순위에 맞춰서요 그러나 앱에 선제적 복원을 구현하기 위해서는 먼저 몇 가지 사항을 고려해야 합니다
여러 개의 제품 또는 구독 그룹을 지원하는 경우 고객의 상태는 각 제품 각 구독 그룹에 따라 다릅니다 따라서 하이브리드 상태나 다른 종속성을 고려해야 할 수도 있습니다 플랫폼 외 활동과 그것이 고객의 제품 상태에 미치는 영향을 고려해 보세요 또한 App Store 서버 알림도 반드시 확인하고요 이것들은 모든 앱 내 구입 유형에 대해 서버들 사이에서 상태를 유지하는 데 매우 중요합니다 버전 2에서는 새로운 알림 유형과 하위 유형은 28개의 고유한 이벤트를 지원합니다 거의 실시간으로 서버에 안전하게 전송되죠 버전 2 통합 또는 마이그레이션에 대해 자세히 알아보려면 '앱 내 구입 통합, 마이그레이션 살펴보기' 세션을 살펴보세요 알렉스와 가브리엘이 StoreKit와 StoreKit 2와의 호환성 및 모범 사례를 보여줄 겁니다 지금까지 고객 제품 상태부터 지원까지 이야기했습니다 그 경험이 고객에게 무엇을 줄 수 있는지도요 이제 구현 세부 정보를 살펴보겠습니다 SK Demo 앱을 사용하겠습니다 StoreKit 2를 사용하여 선제적 복원으로 업데이트했습니다 SK Demo 앱은 이 세션에서 다운로드할 수 있습니다 활성 앱 내 구입이 없는 신규 고객에게 SK Demo 앱이 제공하는 기본 경험을 살펴봅시다 제품을 보기 위해 '쇼핑' 버튼을 탭합니다 상단에 구입할 수 있는 자동차 목록이 있습니다 비소모품 앱 내 구입이죠 길 안내 서비스는 월간 자동 갱신 구독으로 제공됩니다 고객은 세 가지 수준의 서비스 중 선택할 수 있습니다 아래에는 비갱신 구독 옵션이 있습니다 일회성 액세스죠 구매한 제품이 없는 고객을 대상으로 앱을 사용하는 새로운 경험을 제공합니다 이제 앱이 어떻게 고객의 현재 또는 과거 구매 여부를 알 수 있는지 알아봅시다 앱이 실행될 때 세 가지 단계를 실행해야 합니다 가장 중요한 것은 이 단계들은 구매 버튼이 고객에게 보여지기 전에 완료되어야 한다는 것입니다 첫 번째 단계는 앱이 App Store에서 트랜잭션에 대해 듣는 것으로 시작합니다 이건 App Store 모범 사례입니다 트랜잭션은 언제든 일어날 수 있죠 가족 공유, 구매 요청 코드 교환, 구독 자동 갱신 또는 구매가 중단될 때 등에요 추가적으로 앱은 취소된 거래를 수신할 수도 있습니다 환불로 인해 액세스 권한이 상실된 경우나 더 이상 가족 공유가 되지 않는 경우죠 이는 후속 앱이 출시되면 더 많이 적용될 겁니다 이미 액세스 권한이 부여되었는데 상태가 활성에서 비활성으로 바뀌는 경우죠 트랜잭션이 발견되면 완료되지 않은 트랜잭션으로 간주되고 검증을 거쳐 고객에게 전달되고 그리고 완료로 표시됩니다 이렇게 하면 앱이 어떤 트랜잭션도 놓치지 않고 훌륭한 고객 경험을 제공할 수 있습니다 이제 SK Demo 앱이 StoreKit 2에서 트랜잭션을 듣는 방법을 봅시다 여기에서는 listenForTransactions 함수를 사용하고 있습니다 완료되지 않은 트랜잭션 또는 트랜잭션 업데이트를 반환합니다 로그인한 App Store 고객에 대해서요 StoreKit 2는 여기에서 발견된 모든 트랜잭션의 진위를 검증합니다 그런 다음 앱이 콘텐츠를 표시하거나 액세스 권한을 부여하거나 고객 제품 상태를 업데이트한 후 트랜잭션을 마쳐서 구매가 완료되었음을 App Store에 알려줍니다 트랜잭션이 완료되면 모든 기기에서 StoreKit을 통해 더 이상 앱으로 반환되지 않습니다 이 첫 번째 단계는 모든 앱에서 중요합니다 모든 앱이 실행될 때마다 일어날 겁니다 2단계는 고객의 제품 상태를 확인하는 것입니다 이는 고객의 활성 거래를 currentEntitlements를 사용하여 선제적으로 요청하여 실행됩니다 특히 자동 갱신 구독의 경우 취소, 청구 재시도나 다운그레이드 보류와 같은 고객의 업데이트 상태를 설명하기 위해 Product.SubscriptionInfo. RenewalState를 사용할 겁니다 어떻게 하는지 SK Demo 앱에서 살펴보겠습니다 updateCustomerProductStatus 함수로 시작합니다 지속적인 앱 내 구입 유형에 대해 고객의 제품 상태를 추적하는 함수죠 그런 다음 각 구매유형을 StoreKit 2의 currentEntitlements 메서드를 사용해서 반복합니다 그러면 고객이 가져야 할 것 같은 제품의 트랜잭션이 반환됩니다 이 트랜잭션들을 제품 유형별로 기록합니다 여기는 비소모품의 경우고요 여기는 비갱신 구독 제품의 경우죠 활성 가입자인지 비활성 가입자인지 확인하기 위해 비갱신 구독의 경우 만료일을 계산하는 로직을 추가했습니다 마지막으로 활성 자동 갱신 구독이 있는지 확인합니다 그리고 그 상태를 구독 그룹에 적용합니다 청구 재시도, 만료, 취소와 같은 비활성 상태를 설명하는 구독 그룹 상태 변수는 Product.SubscriptionInfo. RenewalState입니다 이제 사용자의 트랜잭션을 알고 있고 각 제품 또는 구독 그룹에 대한 고객 상태도 결정되었습니다 우리 앱에는 다양한 사용 사례별로 앱 경험을 개인화하는 로직이 있습니다 SK Demo 앱 소스 코드를 살펴봅시다 세 가지 앱 내 구입 제품 유형 모두에 대한 활성 트랜잭션이 결정되지 않은 고객이라면 앞서 살펴봤던 새로운 고객 경험을 기본적으로 만나게 됩니다 '쇼핑' 페이지로 이어지는 간단한 클릭 유도 문구가 나타나죠 고객에게 활성 구매가 있는 경우 앱 실행 시 구매 항목이 표시됩니다 그에 따라 모든 제품의 '구매' 버튼이 업데이트됩니다 비소모품의 경우에는 고객이 구매한 제품이 표시됩니다 앱은 고객이 구매한 비소모품을 표시하거나 고객에게 쇼핑 페이지를 방문하도록 요청하죠 활성 제품의 경우 여기에서 고객이 내비게이션 서비스의 활성 가입자인 경우를 다룹니다 비갱신 구독 및 자동 갱신 구독의 경우죠 마지막 부분에서는 비활성 가입자에 대해 다룹니다 구독이 만료되거나 취소된 사용자 또는 청구 재시도 상태에 있는 사용자입니다 이제 SK Demo 앱으로 이동합니다 비소모품, 자동 갱신 구독 모두에 대해 활성 고객을 시뮬레이션하고 싶습니다 즉, Race Car를 구매하고 Pro 내비게이션을 구독하면 데모 앱에는 녹색 체크표시가 나타납니다 앱에서 해당 구매가 성공적으로 확인되었음을 나타내는 거죠 이렇게 구매하면 비소모품에 대한 고객 제품 상태는 구입으로 바뀝니다 여기 구독의 경우 저는 활성 구독자입니다 이제 제가 새 기기에 앱을 설치하고 SK Demo 앱을 처음 실행하면 앱은 1, 2, 3단계를 선제적으로 수행합니다 데모 앱이 제가 구매한 두 항목에 대한 액세스를 선제적으로 복원한 것을 볼 수 있습니다 저는 아무 것도 안 했는데도요 이건 데모 앱이므로 이 정도만 구현 가능합니다 그러나 여러분의 앱에서는 이 프로세스를 통해 활성 고객에게 기존 구매 제품을 보여주지 않고 고객이 이미 소유한 제품과 서비스를 자동 활성화할 수 있어요 현재 고객에게는 좋은 일이죠 고객에게 로그인을 재요청하거나 '구매 항목 복원'을 클릭하라고 요청할 필요가 없습니다 그냥 잘 작동합니다 여러분의 앱에서 바로 사용 가능한 데이터와 API를 사용하면 됩니다 지금까지 StoreKit 2에서 3단계 방식으로 만들어 보았습니다 이번에는 이전 버전의 iOS에서 이와 동일하게 구현하는 방법을 논의해 봅시다 StoreKit 2의 기능을 활용할 수 없는 경우에요 StoreKit을 사용하더라도 고객 제품 상태를 확인하기 위해서는 StoreKit 2와 동일한 단계를 거쳐야 합니다 iOS 7 이상에서 앱 내 구입을 선제적으로 복원하는 거죠 이러려면 서버에서 verifyReceipt 엔드포인트를 사용해야 해요 고객의 제품 상태를 결정하기 위해 최신 트랜잭션을 검증 및 검색해야 하니까요 App Store에서 앱을 설치하면 기기에 앱 영수증이 존재합니다 그러나 샌드박스 또는 TestFlight로 테스트할 때는 앱 영수증은 앱 내 구입이 완료되거나 복원된 후에만 존재합니다 샌드박스로 할 때는 앱 영수증이 없어요 따라서 앱은 이 시나리오를 신규 고객과 같은 경우라고 간주합니다 앱 내 구입이 없으니까요 과거에 만들어진 앱 영수증으로도 App Store에서 최신 거래를 검색할 수 있습니다 따라서 고객에게 '구매 복원'을 요청할 필요도 없고 receiveRefresh도 필요 없습니다 verifyReceipt 요청에 공유 암호를 포함하기만 하면 됩니다 비소모품, 비갱신 구독 자동 갱신 구독에 대한 최신 트랜잭션을 받기 위해서요 앞에서 검토한 세 가지 구현 단계를 다시 살펴보겠습니다 2단계만 달라요 고객의 제품 상태를 식별하는 단계죠 고객 제품 상태를 확인하기 위해 기기의 앱 영수증을 확인하면 서버는 App Store verifyReceipt 엔드포인트로 유효성을 검사합니다 이 과정을 살펴봅시다 먼저 앱 영수증을 가져와야 합니다 반드시 appStoreReceiptURL 속성을 사용하세요 개발자 문서의 이 샘플처럼요 앱 영수증이 어떻게 기기에서 서버와 App Store로 전송되는지 봅시다 여러분의 기기의 앱은 여기 왼쪽에 있는 겁니다 먼저 앱 영수증을 검색하고 그것을 여러분의 서버로 보냅니다 그리고 App Store verifyReceipt 엔드포인트로 유효성을 검사합니다 그 응답을 받아 고객 제품 상태를 결정하고 그 상태를 앱으로 보냅니다 WWDC2020에서 보여드렸던 자격 엔진을 사용하여 고객 제품 상태를 확인했습니다 비소모품, 비갱신 구독을 지원하도록 업데이트되었죠 이제 앱 내 구입이 없는 신규 고객 상태를 처리할 수 있습니다
자격 엔진 사용법에 대해 자세히 알아보려면 '구독을 위한 아키텍트' 세션을 확인해 보세요 샘플 프로젝트를 다운로드하세요 이 동영상의 리소스에 이 세션 및 기타 정보 링크가 있습니다 이렇게 2단계가 완료되었습니다 앱이 서버에서 고객 제품 상태를 수신했죠 이제 여러분의 앱은 구동되는 즉시 StoreKit 2 또는 StoreKit 프레임워크를 사용하여 앱 경험을 개인화할 수 있습니다 최종 모범 사례를 보여드리겠습니다 먼저, 여러분의 앱 내에서 '구매 복원' 버튼을 계속 제공합니다 자주 사용되지는 않지만 고객에게 앱을 강제할 기회를 주는 거죠 문제가 발생했을 때 Apple ID의 트랜잭션을 복원하거나 고객이 다른 Apple ID를 사용하는 경우에요 앱이 기기에서 고객의 앱 내 구입을 선제적으로 복원할 때 앱을 최적화하고 데이터를 안전하게 저장하는 것이 좋습니다 고객 제품 상태를 결정하는 데 도움이 됩니다 유연성, 보안, 고객의 기기에서 동기화하는 기능을 가진 CloudKit를 고려해 보세요 StoreKit을 사용할 때는 구현한 것을 테스트해보는게 중요합니다 StoreKit 2를 사용하면 선제적 복원 구현을 테스트할 수 있습니다 샌드박스, TestFlight, Xcode StoreKit 테스트가 가능합니다 StoreKit을 사용하다면 샌드박스, TestFlight에서 테스트하면 앱 영수증이 없을 수도 있다는 걸 잊지 마세요 하지만 App Store에서 앱을 설치하면 항상 있을 겁니다 앱 영수증이 없으면 앱에서는 신규 고객을 기본값으로 두는 것이 좋습니다 그리고 구매 복원 버튼을 쉽게 사용할 수 있는지 확인하세요 결론적으로 앱을 업데이트하여 구매를 선제적으로 확인하세요 고객이 뭔가를 클릭하거나 인증할 필요가 없게 하세요 앱을 실행하는 순간 신규, 활성 및 비활성 고객의 제품 상태에 따라 맞춤화된 고객의 경험이 표시되도록 만드세요 App Store Server 알림 버전 2를 구현하여 고객의 모든 트랜잭션에 대한 상태와 모든 서버들에서의 모든 앱 내 구입 유형을 유지하세요 이를 통해 여러분의 백엔드는 거의 실시간으로 모든 변경 사항의 트랜잭션 즉 환불 또는 취소된 거래 구독 갱신, 청구 재시도, 만료 등에 대해 알 수 있습니다 시청해주셔서 감사합니다 '앱 내 구입의 새로운 기능' 세션도 잊지 말고 시청하세요 StoreKit, Server API Server Notifications 버전 2의 모든 멋진 업데이트에 대해 대니와 이안이 알려드릴 겁니다 고맙습니다, 안녕히 계세요
-
-
11:16 - Transaction Listener at app launch
//Transaction Listener at app launch func listenForTransactions() -> Task<Void, Error> { return Task.detached { //Iterate through any transactions which didn't come from a direct call to `purchase()`. for await result in Transaction.updates { do { let transaction = try self.checkVerified(result) //Deliver products to the user. await self.updateCustomerProductStatus() //Always finish a transaction await transaction.finish() } catch { //StoreKit transaction failed verification, don't deliver content to user. print("Transaction failed verification") } } } }
-
12:27 - Determine customer product state
//Determine customer product state func updateCustomerProductStatus() async { var purchasedCars: [Product] = [] var purchasedSubscriptions: [Product] = [] var purchasedNonRenewableSubscriptions: [Product] = [] //Iterate through all of the user's purchased products. for await result in Transaction.currentEntitlements { do { //First check if the transaction is verified. If the transaction is not verified //we'll catch the `failedVerification` error. let transaction = try checkVerified(result) //Check the `productType` of the transaction and get the corresponding product from the store. switch transaction.productType { case .nonConsumable: if let car = cars.first(where: { $0.id == transaction.productID }) { purchasedCars.append(car) } //..
-
12:56 - Determine customer product state
//Determine customer product state case .nonRenewable: if let nonRenewable = nonRenewables.first(where: { $0.id == transaction.productID }), transaction.productID == "nonRenewing.standard" { //Non-renewing subscriptions have no inherent expiration. let currentDate = Date() let expirationDate = Calendar(identifier: .gregorian).date(byAdding: DateComponents(year: 1), to: transaction.purchaseDate)! if currentDate < expirationDate { purchasedNonRenewableSubscriptions.append(nonRenewable) } } //..
-
13:09 - Determine customer product state
//Determine customer product state case .autoRenewable: if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) { purchasedSubscriptions.append(subscription) } default: break } } catch { print() } } //Update the Store information with the purchased products. self.purchasedCars = purchasedCars self.purchasedNonRenewableSubscriptions = purchasedNonRenewableSubscriptions self.purchasedSubscriptions = purchasedSubscriptions //Check subscriptionGroupStatus to learn auto-renewable subscription state subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.first?.state }
-
13:45 - Updating my car view at app launch
//Updating my car view at app launch if store.purchasedCars.isEmpty && store.purchasedNonRenewableSubscriptions.isEmpty && store.purchasedSubscriptions.isEmpty { VStack { Text("SK Demo App") .bold() .font(.system(size: 50)) .padding(.bottom, 20) Text("🏎💨") .font(.system(size: 120)) .padding(.bottom, 20) Text("Head over to the shop to get started!") .font(.headline) NavigationLink { StoreView() } //… } } }
-
13:59 - Updating my car view at app launch
//Updating my car view at app launch else { List { Section("My Cars") { if !store.purchasedCars.isEmpty { ForEach(store.purchasedCars) { product in NavigationLink { ProductDetailView(product: product) } label: { ListCellView(product: product, purchasingEnabled: false) } } } else { Text("You don't own any car products. \nHead over to the shop to get started!") } } //…
-
14:20 - Updating my car view at app launch
//Updating my car view at app launch Section("Navigation Service") { if !store.purchasedNonRenewableSubscriptions.isEmpty || !store.purchasedSubscriptions.isEmpty { ForEach(store.purchasedNonRenewableSubscriptions) { product in NavigationLink { ProductDetailView(product: product) } label: { ListCellView(product: product, purchasingEnabled: false) } } ForEach(store.purchasedSubscriptions) { product in NavigationLink { ProductDetailView(product: product) } label: { ListCellView(product: product, purchasingEnabled: false) } } }
-
14:30 - Updating my car view at app launch
//Updating my car view at app launch else { if let subscriptionGroupStatus = store.subscriptionGroupStatus { if subscriptionGroupStatus == .expired || subscriptionGroupStatus == .revoked { Text("Welcome Back! \nHead over to the shop to get started!") } else if subscriptionGroupStatus == .inBillingRetryPeriod { //Provide a deep link from your app to https://apps.apple.com/account/billing. Text("Please verify your billing details.") } } else { Text("You don't own any subscriptions. \nHead over to the shop to get started!") } } }
-
17:42 - Fetch App Receipt Data
//Fetch App Receipt Data public func getReceipt() { if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) print(receiptData) let receiptString = receiptData.base64EncodedString(options: []) print("receipt send it to your server: \(receiptString)") // Read receiptData } catch { print("Couldn't read receipt data with error: " error.localizedDescription) } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.