1. 소개
최종 업데이트: 2023년 7월 11일
Flutter 앱에 인앱 구매를 추가하려면 앱 스토어와 Play 스토어를 올바르게 설정하고 구매를 인증하며 정기 결제 혜택과 같은 필수 권한을 부여해야 합니다.
이 Codelab에서는 세 가지 유형의 인앱 구매를 앱에 추가하고 (개발자에게 제공) Firebase가 포함된 Dart 백엔드를 사용하여 이러한 구매를 확인합니다. 제공된 앱인 Dash Clicker에는 Dash 마스코트를 통화로 사용하는 게임이 포함되어 있습니다. 다음 구매 옵션을 추가합니다.
- 한 번에 2,000개의 Dash를 구매할 수 있는 반복 가능한 구매 옵션입니다.
- 이전 스타일의 Dash를 현대적인 스타일의 Dash로 만들기 위한 일회성 업그레이드 구매입니다.
- 자동으로 생성된 클릭수를 두 배로 늘린 구독
첫 번째 구매 옵션은 사용자에게 2000 Dash라는 직접적인 혜택을 제공합니다. 사용자가 직접 사용할 수 있으며 여러 번 구매할 수 있습니다. 이는 직접 소비되며 여러 번 소비될 수 있으므로 소비성이라고 합니다.
두 번째 옵션은 Dash를 더 아름다운 Dash로 업그레이드합니다. 한 번만 구매하면 영구히 사용할 수 있습니다. 이러한 구매는 앱에서 사용할 수 없지만 영원히 유효하므로 비소비성 구매라고 합니다.
세 번째이자 마지막 구매 옵션은 구독입니다. 정기 결제가 활성 상태인 동안 사용자는 Dashes를 더 빨리 사용할 수 있지만, 사용자가 정기 결제 지불을 중지하면 혜택도 사라집니다.
백엔드 서비스 (또한 사용자에게 제공)는 Dart 앱으로 실행되고 구매가 이루어졌는지 확인하고 Firestore를 사용하여 저장합니다. 프로세스를 더 쉽게 하는 데 Firestore가 사용되지만 프로덕션 앱에서는 모든 유형의 백엔드 서비스를 사용할 수 있습니다.
빌드할 항목
- 소비성 구매 및 정기 결제를 지원하도록 앱을 확장합니다.
- 또한 Dart 백엔드 앱을 확장하여 구매한 아이템을 확인하고 저장합니다.
학습 내용
- 구매 가능한 제품으로 App Store 및 Play 스토어를 구성하는 방법
- 매장과 통신하여 구매를 확인하고 Firestore에 저장하는 방법
- 앱에서 구매를 관리하는 방법
필요한 항목
- Android 스튜디오 4.1 이상
- Xcode 12 이상(iOS 개발용)
- Flutter SDK
2. 개발 환경 설정
이 Codelab을 시작하려면 코드를 다운로드하고 iOS의 번들 식별자와 Android의 패키지 이름을 변경합니다.
코드 다운로드
명령줄에서 GitHub 저장소를 클론하려면 다음 명령어를 사용합니다.
git clone https://github.com/flutter/codelabs.git flutter-codelabs
또는 GitHub의 CLI 도구가 설치되어 있는 경우 다음 명령어를 사용합니다.
gh repo clone flutter/codelabs flutter-codelabs
샘플 코드는 Codelab 모음 코드가 포함된 flutter-codelabs
디렉터리에 클론됩니다. 이 Codelab의 코드는 flutter-codelabs/in_app_purchases
에 있습니다.
flutter-codelabs/in_app_purchases
아래의 디렉터리 구조에는 이름이 지정된 각 단계의 마지막에 있어야 하는 위치에 대한 일련의 스냅샷이 포함되어 있습니다. 시작 코드는 0단계에 있으므로 다음과 같이 일치하는 파일을 쉽게 찾을 수 있습니다.
cd flutter-codelabs/in_app_purchases/step_00
앞으로 건너뛰거나 단계 후 표시되는 내용을 확인하려면 관심 있는 단계의 이름을 딴 디렉토리를 살펴보세요. 마지막 단계의 코드는 complete
폴더 아래에 있습니다.
시작 프로젝트 설정
원하는 IDE의 step_00
에서 시작 프로젝트를 엽니다. 스크린샷에 Android 스튜디오를 사용했지만 Visual Studio Code도 좋은 옵션입니다. 어느 편집기를 사용하든 최신 Dart 및 Flutter 플러그인이 설치되어 있는지 확인합니다.
개발자는 앱 스토어 및 Play 스토어와 통신하여 어떤 제품이 얼마에 얼마에 제공되는지 파악해야 합니다. 모든 앱은 고유 ID로 식별됩니다. iOS App Store의 경우에는 번들 식별자라고 하고 Android Play 스토어의 경우에는 애플리케이션 ID입니다. 이러한 식별자는 일반적으로 역방향 도메인 이름 표기법을 사용하여 만들어집니다. 예를 들어 flutter.dev용 인앱 구매 앱을 만들 때는 dev.flutter.inapppurchase
를 사용합니다. 앱의 식별자를 생각해 보면, 이제 프로젝트 설정에서 이를 설정합니다.
먼저 iOS용 번들 식별자를 설정합니다.
Android 스튜디오에서 프로젝트를 열고 iOS 폴더를 마우스 오른쪽 버튼으로 클릭하고 Flutter를 클릭한 후 Xcode 앱에서 모듈을 엽니다.
Xcode의 폴더 구조에서 Runner 프로젝트가 맨 위에 있고 Runner 프로젝트 아래에 Flutter, Runner, Products 대상이 있습니다. Runner(실행기)를 더블클릭하여 프로젝트 설정을 수정하고 Signing &(서명 및 기능. Team(팀) 필드에서 선택한 번들 식별자를 입력하여 팀을 설정합니다.
이제 Xcode를 닫고 Android 스튜디오로 돌아가서 Android 구성을 완료할 수 있습니다. 이렇게 하려면 android/app,
에서 build.gradle
파일을 열고 applicationId
(아래 스크린샷의 37행)을 iOS 번들 식별자와 동일한 애플리케이션 ID로 변경합니다. iOS 저장소와 Android 저장소의 ID가 동일할 필요는 없지만 동일하게 유지하면 오류가 발생할 가능성이 낮으므로 이 Codelab에서는 동일한 식별자도 사용합니다.
3. 플러그인 설치
Codelab의 이 부분에서는 in_app_purchase 플러그인을 설치합니다.
pubspec에 종속 항목 추가
pubspec의 종속 항목에 in_app_purchase
를 추가하여 pubspec에 in_app_purchase
를 추가합니다.
$ cd app $ flutter pub add in_app_purchase
pubspec.yaml
dependencies:
..
cloud_firestore: ^4.0.3
firebase_auth: ^4.2.2
firebase_core: ^2.5.0
google_sign_in: ^6.0.1
http: ^0.13.4
in_app_purchase: ^3.0.1
intl: ^0.18.0
provider: ^6.0.2
..
pub get을 클릭하여 패키지를 다운로드하거나 명령줄에서 flutter pub get
을 실행합니다.
4. App Store 설정
iOS에서 인앱 구매를 설정하고 테스트하려면 App Store에서 새 앱을 만들어 구매 가능한 제품을 만들어야 합니다. 검토를 위해 앱을 게시하거나 Apple에 전송할 필요가 없습니다. 이렇게 하려면 개발자 계정이 필요합니다. 계정이 없는 경우 Apple 개발자 프로그램에 등록합니다.
유료 앱 계약
인앱 구매를 사용하려면 App Store Connect에서 유료 앱에 대한 유효한 계약도 있어야 합니다. https://appstoreconnect.apple.com/으로 이동하여 약관, 세금 및 은행 거래를 클릭합니다.
무료 및 유료 앱에 관한 계약이 여기에 표시됩니다. 무료 앱은 활성 상태이고 유료 앱의 상태는 새 상태입니다. 약관을 읽고 동의한 후 모든 필수 정보를 입력해야 합니다.
모든 항목이 올바르게 설정되면 유료 앱 상태가 활성화됩니다. 활성 계약이 없으면 인앱 구매를 시도해 볼 수 없기 때문에 이 단계는 매우 중요합니다.
앱 ID 등록
Apple 개발자 포털에서 새 식별자를 만듭니다.
앱 ID 선택
앱 선택
설명을 입력하고 번들 ID가 이전에 XCode에서 설정한 값과 일치하도록 번들 ID를 설정합니다.
새로운 앱 ID를 만드는 방법을 자세히 알아보려면 개발자 계정 도움말을 참고하세요 .
새 앱 만들기
App Store Connect에서 고유한 번들 식별자로 새 앱을 만듭니다.
새 앱을 만들고 계약을 관리하는 방법을 자세히 알아보려면 App Store Connect 도움말을 참고하세요.
인앱 구매를 테스트하려면 샌드박스 테스트 사용자가 필요합니다. 테스트 사용자는 iTunes에 연결하지 말고 인앱 구매 테스트용으로만 사용해야 합니다. Apple 계정에서 이미 사용 중인 이메일 주소는 사용할 수 없습니다. 사용자 및 액세스에서 샌드박스 아래의 테스터로 이동하여 새 샌드박스 계정을 만들거나 기존 샌드박스 Apple ID를 관리합니다.
이제 iPhone에서 Settings(설정) > 앱 스토어 > 샌드박스 계정
인앱 구매 구성
이제 구매 가능한 세 가지 항목을 구성합니다.
dash_consumable_2k
: 여러 번 구매할 수 있는 소비성 구매로, 사용자에게 구매당 2,000개의 Dash (인앱 통화)가 제공됩니다.dash_upgrade_3d
: 비소비성 '업그레이드' 사용자가 클릭할 수 있는 외관상 다른 Dash를 제공합니다.dash_subscription_doubler
: 구독 기간 동안 사용자에게 클릭당 2배 많은 대시를 부여하는 구독입니다.
인앱 구매 > 관리를 클릭합니다.
지정된 ID로 인앱 구매를 만듭니다.
dash_consumable_2k
를 소모성으로 설정합니다.
dash_consumable_2k
를 제품 ID로 사용합니다. 참조 이름은 App Store Connect에서만 사용됩니다. dash consumable 2k
로 설정하고 구매에 사용할 현지화를 추가하기만 하면 됩니다. 2000 dashes fly out
를 설명으로 사용하여 구매 Spring is in the air
를 호출합니다.
dash_upgrade_3d
를 비소비성으로 설정합니다.
dash_upgrade_3d
를 제품 ID로 사용합니다. 참조 이름을 dash upgrade 3d
로 설정하고 구매에 사용할 현지화를 추가합니다. Brings your dash back to the future
를 설명으로 사용하여 구매 3D Dash
를 호출합니다.
dash_subscription_doubler
을(를) 자동 갱신 정기 결제로 설정합니다.
정기 결제의 흐름은 약간 다릅니다. 먼저 참조 이름과 제품 ID를 설정해야 합니다.
다음으로 구독 그룹을 만들어야 합니다. 여러 구독이 동일한 그룹에 속한 경우 사용자는 한 번에 이러한 구독 중 하나만 구독할 수 있으며 각 구독 간에 쉽게 업그레이드 또는 다운그레이드할 수 있습니다. 이 그룹의 이름은 subscriptions
입니다.
다음으로 정기 결제 기간과 현지화를 입력합니다. 이 정기 결제의 이름을 Jet Engine
로 지정하고 설명을 Doubles your clicks
으로 지정합니다. 저장을 클릭합니다.
저장 버튼을 클릭한 후 구독 가격을 추가합니다. 원하는 가격을 선택하세요.
이제 구매 목록에 3개의 구매가 표시됩니다.
5. Play 스토어 설정
App Store와 마찬가지로 Play 스토어용 개발자 계정도 필요합니다. 아직 계정이 없으면 계정을 등록합니다.
새 앱 만들기
Google Play Console에서 새 앱을 만듭니다.
- Play Console을 엽니다.
- 모든 앱 > 앱 만들기
- 기본 언어를 선택하고 앱 제목을 추가합니다. Google Play에 표시하려는 앱 이름을 입력합니다. 이름은 나중에 변경할 수 있습니다.
- 애플리케이션이 게임임을 명시합니다. 나중에 변경할 수 있습니다.
- 애플리케이션이 무료인지 유료인지 지정합니다.
- Play 스토어 사용자가 이 애플리케이션에 관해 문의할 때 사용할 이메일 주소를 추가합니다.
- 콘텐츠 가이드라인 및 미국 수출법 선언을 작성합니다.
- 앱 만들기를 선택합니다.
앱을 만든 후 대시보드로 이동하여 앱 설정 섹션의 모든 작업을 완료합니다. 여기에서 콘텐츠 등급 및 스크린샷과 같은 앱에 관한 정보를 제공합니다.
애플리케이션 서명
인앱 구매를 테스트하려면 Google Play에 빌드를 하나 이상 업로드해야 합니다.
이를 위해서는 릴리스 빌드를 디버그 키가 아닌 다른 것으로 서명해야 합니다.
키 저장소 만들기
기존 키 저장소가 있는 경우 다음 단계로 건너뜁니다. 그렇지 않은 경우 명령줄에서 다음을 실행하여 만듭니다.
Mac/Linux에서는 다음 명령어를 사용합니다.
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
Windows의 경우 다음 명령어를 사용합니다.
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
이 명령어는 홈 디렉터리에 key.jks
파일을 저장합니다. 파일을 다른 곳에 저장하려면 -keystore
매개변수에 전달하는 인수를 변경합니다. 유지하는 것이
keystore
파일 비공개 공개 소스 제어에 체크인하지 마세요.
앱에서 키 저장소 참조
키 저장소에 대한 참조가 포함된 <your app dir>/android/key.properties
라는 파일을 만듭니다.
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
Gradle에서 서명 구성
<your app dir>/android/app/build.gradle
파일을 수정하여 앱 서명을 구성합니다.
android
블록 앞에 속성 파일의 키 저장소 정보를 추가합니다.
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
key.properties
파일을 keystoreProperties
객체에 로드합니다.
buildTypes
블록 앞에 다음 코드를 추가합니다.
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
서명 구성 정보로 모듈의 build.gradle
파일에서 signingConfigs
블록을 구성합니다.
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
이제 앱의 출시 빌드가 자동으로 서명됩니다.
앱 서명에 관한 자세한 내용은 developer.android.com에서 앱 서명을 참고하세요.
첫 번째 빌드 업로드
앱에 서명을 구성한 후에는 다음을 실행하여 애플리케이션을 빌드할 수 있어야 합니다.
flutter build appbundle
이 명령어는 기본적으로 출시 빌드를 생성하며 출력은 <your app dir>/build/app/outputs/bundle/release/
에서 확인할 수 있습니다.
Google Play Console의 대시보드에서 출시 > 테스트 > 비공개 테스트로 이동하여 새로운 비공개 테스트 버전을 만듭니다.
이 Codelab에서는 계속해서 Google에 앱에 서명하므로 Play 앱 서명에서 계속을 눌러 선택합니다.
다음으로, 빌드 명령어로 생성된 app-release.aab
App Bundle을 업로드합니다.
저장을 클릭한 다음 버전 검토를 클릭합니다.
마지막으로 내부 테스트로 출시 시작을 클릭하여 내부 테스트 버전을 활성화합니다.
테스트 사용자 설정
인앱 구매를 테스트하려면 테스터의 Google 계정을 Google Play Console의 다음 두 위치에 추가해야 합니다.
- 특정 테스트 트랙으로 (내부 테스트)
- 라이선스 테스터로서
먼저 내부 테스트 트랙에 테스터를 추가합니다. 출시 > 테스트 > 내부 테스트를 클릭하고 테스터 탭을 클릭합니다.
이메일 목록 만들기를 클릭하여 새 이메일 목록을 만듭니다. 목록 이름을 지정하고 인앱 구매 테스트에 액세스해야 하는 Google 계정의 이메일 주소를 추가합니다.
그런 다음 목록의 체크박스를 선택하고 변경사항 저장을 클릭합니다.
그런 다음 라이선스 테스터를 추가합니다.
- Google Play Console의 모든 앱 보기로 돌아갑니다.
- 설정 > 라이선스 테스트.
- 인앱 구매를 테스트할 수 있어야 하는 테스터와 동일한 이메일 주소를 추가합니다.
- 라이선스 응답을
RESPOND_NORMALLY
로 설정합니다. - 변경사항 저장을 클릭합니다.
인앱 구매 구성
이제 앱 내에서 구매할 수 있는 상품을 구성해 보겠습니다.
App Store에서와 마찬가지로 세 가지 구매를 정의해야 합니다.
dash_consumable_2k
: 여러 번 구매할 수 있는 소비성 구매로, 사용자에게 구매당 2,000개의 Dash (인앱 통화)가 제공됩니다.dash_upgrade_3d
: 비소비성 '업그레이드' 사용자가 클릭할 수 있는 외관상 다른 Dash를 제공합니다.dash_subscription_doubler
: 구독 기간 동안 사용자에게 클릭당 2배 많은 대시를 부여하는 구독입니다.
먼저 소비성 및 비소비성 항목을 추가합니다.
- Google Play Console로 이동하여 애플리케이션을 선택합니다.
- 수익 창출 > 제품 > 인앱 상품
- 제품 만들기를 클릭합니다.
- 제품에 대한 모든 필수 정보를 입력합니다. 제품 ID가 사용하려는 ID와 정확하게 일치하는지 확인합니다.
- 저장을 클릭합니다.
- 활성화를 클릭합니다.
- 비소비성 '업그레이드'에 대해 이 프로세스를 반복합니다. 있습니다.
그런 다음 구독을 추가합니다.
- Google Play Console로 이동하여 애플리케이션을 선택합니다.
- 수익 창출 > 제품 > 정기 결제.
- 구독 만들기를 클릭합니다.
- 구독에 필요한 모든 정보를 입력합니다. 제품 ID가 사용하려는 ID와 정확하게 일치하는지 확인합니다.
- 저장을 클릭합니다.
이제 Play Console에서 구매 항목을 설정할 수 있습니다.
6. Firebase 설정
이 Codelab에서는 백엔드 서비스를 사용하여 사용자의 요청을 확인하고 추적합니다. 구매.
백엔드 서비스를 사용하면 다음과 같은 몇 가지 이점이 있습니다.
- 안전하게 거래를 확인할 수 있습니다.
- 앱 스토어의 결제 이벤트에 반응할 수 있습니다.
- 데이터베이스에서 구매 내역을 추적할 수 있습니다.
- 사용자가 시스템 시계를 되감아 앱이 프리미엄 기능을 제공하도록 속일 수 없습니다.
백엔드 서비스를 설정하는 방법에는 여러 가지가 있지만 Google의 자체 Firebase를 사용하여 Cloud Functions와 Firestore를 사용하여 설정합니다.
백엔드 작성은 이 Codelab의 범위를 벗어나는 것으로 간주되므로 시작 코드에는 이미 기본적인 구매를 처리하여 시작하는 데 도움이 되는 Firebase 프로젝트가 포함되어 있습니다.
Firebase 플러그인은 시작 앱에도 포함되어 있습니다.
이제 Firebase 프로젝트를 직접 만들고 Firebase용 앱과 백엔드를 모두 구성한 후 마지막으로 백엔드를 배포하기만 하면 됩니다.
Firebase 프로젝트 만들기
Firebase Console로 이동하여 새 Firebase 프로젝트를 만듭니다. 이 예에서는 프로젝트의 이름을 Dash Clicker라고 지정합니다.
백엔드 앱에서는 구매를 특정 사용자와 연결하므로 인증이 필요합니다. 이를 위해 Firebase의 인증 모듈과 Google 로그인을 활용합니다.
- Firebase 대시보드에서 인증으로 이동하여 필요한 경우 사용 설정합니다.
- 로그인 방법 탭으로 이동하여 Google 로그인 제공업체를 사용 설정합니다.
Firebase의 Firestore 데이터베이스도 사용하게 되므로 이 항목도 사용 설정합니다.
Cloud Firestore 규칙을 다음과 같이 설정합니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /purchases/{purchaseId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId
}
}
}
Flutter용 Firebase 설정
Flutter 앱에 Firebase를 설치할 때 권장되는 방법은 FlutterFire CLI를 사용하는 것입니다. 설정 페이지에 설명된 안내를 따릅니다.
flutterfire 구성을 실행할 때 이전 단계에서 방금 만든 프로젝트를 선택합니다.
$ flutterfire configure
i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
other-flutter-codelab-1 (other-flutter-codelab-1)
other-flutter-codelab-2 (other-flutter-codelab-2)
other-flutter-codelab-3 (other-flutter-codelab-3)
other-flutter-codelab-4 (other-flutter-codelab-4)
<create a new project>
그런 다음 두 플랫폼을 선택하여 iOS와 Android를 사용 설정합니다.
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
firebase_options.dart를 재정의할지 묻는 메시지가 표시되면 예를 선택합니다.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Android용 Firebase 설정: 추가 단계
Firebase 대시보드에서 프로젝트 개요로 이동하고 설정을 선택한 다음 일반 탭을 선택합니다.
내 앱까지 아래로 스크롤한 다음 dashclicker (Android) 앱을 선택합니다.
디버그 모드에서 Google 로그인을 허용하려면 디버그 인증서의 SHA-1 해시 지문을 제공해야 합니다.
디버그 서명 인증서 해시 가져오기
Flutter 앱 프로젝트의 루트에서 디렉터리를 android/
폴더로 변경한 후 서명 보고서를 생성합니다.
cd android ./gradlew :app:signingReport
방대한 서명 키 목록이 표시됩니다. 디버그 인증서의 해시를 찾고 있으므로 Variant
및 Config
속성이 debug
로 설정된 인증서를 찾습니다. 키 저장소는 홈 폴더의 .android/debug.keystore
아래에 있을 가능성이 높습니다.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
SHA-1 해시를 복사하고 앱 제출 모달 대화상자의 마지막 필드를 작성합니다.
iOS용 Firebase 설정: 추가 단계
Xcode
로 ios/Runnder.xcworkspace
를 엽니다. 또는 원하는 IDE를 사용할 수도 있습니다.
VSCode에서 ios/
폴더를 마우스 오른쪽 버튼으로 클릭한 다음 open in xcode
를 클릭합니다.
Android 스튜디오에서 ios/
폴더를 마우스 오른쪽 버튼으로 클릭하고 flutter
, open iOS module in Xcode
옵션을 차례로 클릭합니다.
iOS에서 Google 로그인을 허용하려면 빌드 plist
파일에 CFBundleURLTypes
구성 옵션을 추가합니다. 자세한 내용은 google_sign_in
패키지 문서를 참고하세요. 이 경우 파일은 ios/Runner/Info-Debug.plist
및 ios/Runner/Info-Release.plist
입니다.
키-값 쌍이 이미 추가되었지만 그 값을 다음과 같이 바꿔야 합니다.
- 주변의
<string>..</string>
요소 없이GoogleService-Info.plist
파일에서REVERSED_CLIENT_ID
값을 가져옵니다. CFBundleURLTypes
키 아래에서ios/Runner/Info-Debug.plist
및ios/Runner/Info-Release.plist
파일의 값을 모두 바꿉니다.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
이제 Firebase 설정이 완료되었습니다.
7. 구매 관련 업데이트 듣기
Codelab의 이 부분에서는 제품 구매를 위한 앱을 준비합니다. 이 프로세스에는 앱 시작 후 구매 업데이트 및 오류 수신 대기가 포함됩니다.
구매 관련 최신 소식 듣기
main.dart,
에서 두 페이지가 포함된 BottomNavigationBar
가 있는 Scaffold
가 있는 MyHomePage
위젯을 찾습니다. 이 페이지에서는 DashCounter
, DashUpgrades,
, DashPurchases
에 관한 세 개의 Provider
도 만듭니다. DashCounter
는 현재 대시 수를 추적하고 이를 자동으로 증가시킵니다. DashUpgrades
는 Dashes로 구매할 수 있는 업그레이드를 관리합니다. 이 Codelab에서는 DashPurchases
에 중점을 둡니다.
기본적으로 제공자의 객체는 해당 객체가 처음 요청될 때 정의됩니다. 이 객체는 앱이 시작될 때 직접 구매 업데이트를 리슨하므로 lazy: false
를 사용하여 이 객체에서 지연 로드를 사용 중지합니다.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false,
),
InAppPurchaseConnection
의 인스턴스도 필요합니다. 그러나 앱을 테스트 가능한 상태로 유지하려면 연결을 모의 처리하는 방법이 필요합니다. 이렇게 하려면 테스트에서 재정의할 수 있는 인스턴스 메서드를 만들어 main.dart
에 추가합니다.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
테스트를 계속 실행하려면 테스트를 약간 업데이트해야 합니다. TestIAPConnection
의 전체 코드는 GitHub의 widget_test.dart를 확인합니다.
test/widget_test.dart
void main() {
testWidgets('App starts', (WidgetTester tester) async {
IAPConnection.instance = TestIAPConnection();
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
lib/logic/dash_purchases.dart
에서 DashPurchases ChangeNotifier
코드로 이동합니다. 현재 구매한 Dash에 추가할 수 있는 DashCounter
개만 있습니다.
스트림 구독 속성 _subscription
(StreamSubscription<List<PurchaseDetails>> _subscription;
유형), IAPConnection.instance,
, 가져오기를 추가합니다. 결과 코드는 다음과 같습니다.
lib/logic/dash_purchases.dart
import 'package:in_app_purchase/in_app_purchase.dart';
class DashPurchases extends ChangeNotifier {
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter);
}
_subscription
가 생성자에서 초기화되었으므로 late
키워드가 _subscription
에 추가됩니다. 이 프로젝트는 기본적으로 null을 허용하지 않도록 설정되어 있습니다 (NNBD). 즉, null을 허용하는 것으로 선언되지 않은 속성은 null이 아닌 값을 가져야 합니다. late
한정자를 사용하면 이 값의 정의를 지연할 수 있습니다.
생성자에서 purchaseUpdatedStream
를 가져와서 스트림 수신 대기를 시작합니다. dispose()
메서드에서 스트림 구독을 취소합니다.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
Future<void> buy(PurchasableProduct product) async {
// omitted
}
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
}
}
이제 앱이 구매 업데이트를 수신하므로 다음 섹션에서 구매합니다.
계속하기 전에 "flutter test"
"로 테스트를 실행하여 모든 것이 올바르게 설정되었는지 확인합니다.
$ flutter test
00:01 +1: All tests passed!
8. 구매
Codelab의 이 부분에서는 현재 기존 모의 제품을 실제 구매 가능한 제품으로 바꿉니다. 이러한 제품은 목록에서 로드되어 표시되며 제품을 탭하면 구매됩니다.
Adapt PurchasableProduct
PurchasableProduct
는 모의 제품을 표시합니다. purchasable_product.dart
의 PurchasableProduct
클래스를 다음 코드로 바꿔 실제 콘텐츠를 표시하도록 업데이트합니다.
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
dash_purchases.dart,
에서 더미 구매를 삭제하고 빈 목록(List<PurchasableProduct> products = [];
)으로 바꿉니다.
구매 가능한 구매 항목 로드하기
사용자가 구매할 수 있도록 하려면 스토어에서 구매 항목을 로드합니다. 먼저 매장의 이용 가능 여부를 확인합니다. 스토어를 사용할 수 없는 경우 storeState
를 notAvailable
로 설정하면 사용자에게 오류 메시지가 표시됩니다.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
스토어를 사용할 수 있게 되면 구매 가능한 항목을 로드합니다. 이전 Firebase 설정을 고려할 때 storeKeyConsumable
, storeKeySubscription,
, storeKeyUpgrade
가 표시될 것으로 예상됩니다. 예상 구매를 확인할 수 없는 경우 이 정보를 콘솔에 출력합니다. 이 정보를 백엔드 서비스로 보내는 것이 좋습니다.
await iapConnection.queryProductDetails(ids)
메서드는 찾을 수 없는 ID와 발견된 구매 가능한 제품을 모두 반환합니다. 응답의 productDetails
를 사용하여 UI를 업데이트하고 StoreState
를 available
로 설정합니다.
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
for (var element in response.notFoundIDs) {
debugPrint('Purchase $element not found');
}
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
생성자에서 loadPurchases()
함수를 호출합니다.
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
마지막으로 storeState
필드 값을 StoreState.available
에서 StoreState.loading:
로 변경합니다.
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
구매 가능한 제품 표시
purchase_page.dart
파일을 살펴보겠습니다. PurchasePage
위젯은 StoreState
에 따라 _PurchasesLoading
, _PurchaseList,
또는 _PurchasesNotAvailable,
를 표시합니다. 위젯에는 다음 단계에서 사용되는 사용자의 이전 구매도 표시됩니다.
_PurchaseList
위젯은 구매 가능한 제품 목록을 표시하고 DashPurchases
객체로 구매 요청을 보냅니다.
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
제품이 올바르게 구성된 경우 Android 및 iOS 스토어에서 구매할 수 있는 제품을 확인할 수 있습니다. 각 콘솔에 입력했을 때 구매 항목이 가능해지려면 다소 시간이 걸릴 수 있습니다.
dash_purchases.dart
로 돌아가서 제품을 구매하는 함수를 구현합니다. 소비성 제품과 비소비성 제품만 분리하면 됩니다. 업그레이드 및 정기 결제 제품은 비소비성 제품입니다.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
break;
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
계속하기 전에 _beautifiedDashUpgrade
변수를 만들고 이 변수를 참조하도록 beautifiedDash
getter를 업데이트합니다.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
_onPurchaseUpdate
메서드는 구매 업데이트를 수신하고, 구매 페이지에 표시된 제품 상태를 업데이트하고, 구매를 카운터 로직에 적용합니다. 구매를 처리한 후에는 completePurchase
를 호출하여 구매가 올바르게 처리된다는 것을 스토어에서 확인하는 것이 중요합니다.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(2000);
break;
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
break;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. 백엔드 설정
구매 추적 및 확인으로 이동하기 전에 이를 지원하도록 Dart 백엔드를 설정합니다.
이 섹션에서는 dart-backend/
폴더에서 루트로 작업합니다.
다음 도구가 설치되어 있는지 확인하세요.
- Dart
- Firebase CLI
기본 프로젝트 개요
이 프로젝트의 일부는 이 Codelab의 범위를 벗어나는 것으로 간주되므로 시작 코드에 포함되어 있습니다. 시작하기 전에 시작 코드에 있는 내용을 살펴보고 구성 방법을 파악하는 것이 좋습니다.
이 백엔드 코드는 머신에서 로컬로 실행할 수 있으며 사용하기 위해 배포할 필요가 없습니다. 그러나 개발 기기 (Android 또는 iPhone)에서 서버가 실행될 컴퓨터에 연결할 수 있어야 합니다. 이를 위해서는 기기가 동일한 네트워크에 있어야 하며 머신의 IP 주소를 알아야 합니다.
다음 명령어를 사용하여 서버를 실행해 봅니다.
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
Dart 백엔드는 shelf
및 shelf_router
를 사용하여 API 엔드포인트를 제공합니다. 기본적으로 서버는 경로를 제공하지 않습니다. 나중에 구매 인증 프로세스를 처리할 경로를 만듭니다.
시작 코드에 이미 포함된 한 부분은 lib/iap_repository.dart
의 IapRepository
입니다. Firestore 또는 일반적인 데이터베이스와 상호작용하는 방법을 배우는 것은 이 Codelab과 관련이 없는 것으로 여겨집니다. 따라서 시작 코드에는 Firestore에서 구매를 만들거나 업데이트하는 데 사용할 수 있는 함수와 이러한 구매의 모든 클래스가 포함되어 있습니다.
Firebase 액세스 설정
Firebase Firestore에 액세스하려면 서비스 계정 액세스 키가 필요합니다. Firebase 프로젝트 설정을 여는 비공개 키를 생성하고 서비스 계정 섹션으로 이동한 다음 새 비공개 키 생성을 선택합니다.
다운로드한 JSON 파일을 assets/
폴더에 복사하고 이름을 service-account-firebase.json
로 바꿉니다.
Google Play 액세스 설정
구매를 인증하기 위해 Play 스토어에 액세스하려면 이러한 권한을 가진 서비스 계정을 생성하고 JSON 사용자 인증 정보를 다운로드해야 합니다.
- Google Play Console로 이동하여 모든 앱 페이지에서 시작합니다.
- 설정 > API 액세스 Google Play Console에서 프로젝트를 만들거나 기존 프로젝트에 연결하도록 요청하는 경우 먼저 프로젝트를 만들거나 이 페이지로 돌아오세요.
- 서비스 계정을 정의할 수 있는 섹션을 찾아 새 서비스 계정 만들기를 클릭합니다.
- 팝업 대화상자에서 Google Cloud Platform 링크를 클릭합니다. <ph type="x-smartling-placeholder"></ph>
- 프로젝트를 선택합니다. Google 계정이 표시되지 않으면 오른쪽 상단의 계정 드롭다운 목록에서 올바른 Google 계정에 로그인했는지 확인합니다. <ph type="x-smartling-placeholder"></ph>
- 프로젝트를 선택한 후 상단 메뉴 바에서 + 서비스 계정 만들기를 클릭합니다. <ph type="x-smartling-placeholder"></ph>
- 서비스 계정의 이름을 입력하고 필요한 경우 기억할 수 있도록 설명을 입력한 후 다음 단계로 이동합니다.
- 서비스 계정에 편집자 역할을 할당합니다. <ph type="x-smartling-placeholder"></ph>
- 마법사를 종료하고 개발자 콘솔의 API 액세스 페이지로 돌아가서 서비스 계정 새로고침을 클릭합니다. 새로 만든 계정이 목록에 표시됩니다.
- 새 서비스 계정에 대한 액세스 권한 부여를 클릭합니다.
- 다음 페이지에서 아래로 스크롤하여 재무 데이터 블록으로 이동합니다. 재무 데이터, 주문, 취소 설문조사 응답 보기와 주문 및 구독 관리를 모두 선택합니다. <ph type="x-smartling-placeholder"></ph>
- 사용자 초대를 클릭합니다. <ph type="x-smartling-placeholder"></ph>
- 이제 계정이 설정되었으므로 사용자 인증 정보를 생성하기만 하면 됩니다. Cloud 콘솔로 돌아가 서비스 계정 목록에서 서비스 계정을 찾아 세로로 나열된 점 3개를 클릭한 후 키 관리를 선택합니다. <ph type="x-smartling-placeholder"></ph>
- 새 JSON 키를 만들고 다운로드합니다.
- 다운로드한 파일의 이름을
service-account-google-play.json,
로 바꾸고assets/
디렉터리로 이동합니다.
한 가지 더 해야 할 일은 lib/constants.dart,
를 열고 androidPackageId
값을 Android 앱에 선택한 패키지 ID로 바꾸는 것입니다.
Apple App Store 액세스 설정
구매를 인증하기 위해 App Store에 액세스하려면 공유 비밀번호를 설정해야 합니다.
- App Store Connect를 엽니다.
- 내 앱으로 이동하여 앱을 선택합니다.
- 사이드바 탐색 메뉴에서 인앱 구매 > 관리를 클릭합니다.
- 목록 오른쪽 상단에서 App-Specific Shared Secret(앱별 공유 비밀번호)을 클릭합니다.
- 새 보안 비밀을 생성하고 복사합니다.
lib/constants.dart,
를 열고appStoreSharedSecret
값을 방금 생성한 공유 보안 비밀로 바꿉니다.
상수 구성 파일
계속하기 전에 lib/constants.dart
파일에 다음 상수가 구성되어 있는지 확인합니다.
androidPackageId
: Android에서 사용되는 패키지 ID입니다. 예:com.example.dashclicker
appStoreSharedSecret
: 구매 인증을 수행하기 위해 App Store Connect에 액세스하는 공유 비밀번호입니다.bundleId
: iOS에서 사용되는 번들 ID입니다. 예:com.example.dashclicker
당분간 나머지 상수는 무시해도 됩니다.
10. 구매 인증
구매를 인증하는 일반적인 과정은 iOS와 Android가 비슷합니다.
두 상점 모두 구매가 이루어질 때 애플리케이션에서 토큰을 수신합니다.
이 토큰은 앱에서 백엔드 서비스로 전송된 다음 백엔드 서비스에서 제공된 토큰을 사용하여 각 매장의 서버에서 구매를 확인합니다.
그러면 백엔드 서비스는 구매를 저장하고 구매가 유효한지 여부를 애플리케이션에 회신할 수 있습니다.
백엔드 서비스가 사용자 기기에서 실행되는 애플리케이션이 아닌 스토어에서 유효성 검사를 수행하도록 하면 시스템 시계를 되감는 등의 방법으로 사용자가 프리미엄 기능에 액세스하는 것을 방지할 수 있습니다.
Flutter 측 설정
인증을 설정합니다.
백엔드 서비스로 구매를 전송하게 되므로 구매하는 동안 사용자가 인증되었는지 확인하려고 합니다. 대부분의 인증 로직은 시작 프로젝트에 이미 추가되어 있으므로 사용자가 아직 로그인하지 않았을 때 PurchasePage
에 로그인 버튼이 표시되는지 확인하기만 하면 됩니다. PurchasePage
의 빌드 메서드 시작 부분에 다음 코드를 추가합니다.
lib/pages/purchase_page.dart
import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
앱에서 통화 확인 엔드포인트
앱에서 http 사후 호출을 사용하여 Dart 백엔드에서 /verifypurchase
엔드포인트를 호출하는 _verifyPurchase(PurchaseDetails purchaseDetails)
함수를 만듭니다.
선택한 스토어 (Play 스토어의 경우 google_play
, App Store의 경우 app_store
), serverVerificationData
, productID
을 전송합니다. 서버에서 구매가 인증되었는지 여부를 나타내는 상태 코드를 반환합니다.
앱 상수에서 서버 IP를 로컬 머신 IP 주소로 구성합니다.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
main.dart:
에서 DashPurchases
를 생성하여 firebaseNotifier
를 추가합니다.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
사용자 ID를 Verify purchase 함수에 전달할 수 있도록 FirebaseNotifier에서 사용자에 대한 getter를 추가합니다.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
_verifyPurchase
함수를 DashPurchases
클래스에 추가합니다. 이 async
함수는 구매가 검증되었는지 여부를 나타내는 불리언을 반환합니다.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
print('Successfully verified purchase');
return true;
} else {
print('failed request: ${response.statusCode} - ${response.body}');
return false;
}
}
구매를 적용하기 직전에 _handlePurchase
에서 _verifyPurchase
함수를 호출합니다. 인증이 완료된 후에만 구매를 적용해야 합니다. 프로덕션 앱에서는 이를 추가로 지정하여 스토어를 일시적으로 사용할 수 없을 때 무료 체험판 정기 결제를 적용할 수 있습니다. 하지만 이 예시에서는 단순하게 유지하고 구매가 성공적으로 확인된 경우에만 구매를 적용하세요.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(1000);
break;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
이제 앱에서 모든 것이 구매를 검증할 준비가 되었습니다.
백엔드 서비스 설정
다음으로 백엔드에서 구매를 확인하도록 Cloud 함수를 설정합니다.
구매 핸들러 빌드
두 스토어의 인증 흐름이 거의 동일하므로 각 매장에 별도의 구현을 사용하여 추상적인 PurchaseHandler
클래스를 설정합니다.
먼저 lib/
폴더에 purchase_handler.dart
파일을 추가합니다. 여기서는 정기 결제와 비정기 결제라는 두 가지 구매 유형을 확인하는 두 가지 추상적인 메서드가 있는 추상 PurchaseHandler
클래스를 정의합니다.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
보시다시피 각 메서드에는 세 가지 매개변수가 필요합니다.
userId:
: 구매 항목을 사용자와 연결할 수 있는 로그인한 사용자의 ID입니다.productData:
제품 관련 데이터 잠시 후에 이를 정의할 것입니다.token:
: 스토어에서 사용자에게 제공한 토큰입니다.
또한 이러한 구매 핸들러를 더 쉽게 사용하려면 정기 결제와 비 정기 결제 모두에 사용할 수 있는 verifyPurchase()
메서드를 추가합니다.
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
이제 두 경우 모두 verifyPurchase
를 호출할 수 있지만 여전히 별도의 구현이 있습니다.
ProductData
클래스에는 제품 ID (SKU라고도 함) 및 ProductType
를 비롯하여 구매 가능한 여러 제품에 관한 기본 정보가 포함됩니다.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ProductType
는 정기 결제이거나 비정기 결제일 수 있습니다.
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
마지막으로, 제품 목록은 같은 파일에서 지도로 정의됩니다.
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
다음으로 Google Play 스토어 및 Apple App Store의 자리표시자 구현을 정의합니다. Google Play로 시작하기:
lib/google_play_purchase_handler.dart
를 만들고 방금 작성한 PurchaseHandler
를 확장하는 클래스를 추가합니다.
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
지금은 핸들러 메서드에 true
를 반환합니다. 나중에 다룰 것입니다.
앞서 살펴본 바와 같이 생성자는 IapRepository
의 인스턴스를 사용합니다. 구매 핸들러는 나중에 이 인스턴스를 사용하여 Firestore에 구매에 대한 정보를 저장합니다. Google Play와 통신하려면 제공된 AndroidPublisherApi
를 사용합니다.
그런 다음 앱 스토어 핸들러에 대해서도 동일한 작업을 수행합니다. lib/app_store_purchase_handler.dart
를 만들고 PurchaseHandler
를 다시 확장하는 클래스를 추가합니다.
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
좋습니다. 이제 구매 핸들러가 두 개입니다. 다음으로 구매 확인 API 엔드포인트를 만들어 보겠습니다.
구매 핸들러 사용
bin/server.dart
를 열고 shelf_route
를 사용하여 API 엔드포인트를 만듭니다.
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
위의 코드는 다음을 실행합니다.
- 이전에 만든 앱에서 호출할 POST 엔드포인트를 정의합니다.
- JSON 페이로드를 디코딩하고 다음 정보를 추출합니다.
userId
: 현재 로그인한 사용자 IDsource
: 사용된 저장소(app_store
또는google_play
)productData
: 이전에 만든productDataMap
에서 가져옵니다.token
: 매장으로 전송할 인증 데이터가 포함됩니다.- 소스에 따라
GooglePlayPurchaseHandler
또는AppStorePurchaseHandler
에서verifyPurchase
메서드를 호출합니다. - 확인에 성공하면 메서드가 클라이언트에
Response.ok
를 반환합니다. - 확인에 실패하면 메서드는 클라이언트에
Response.internalServerError
를 반환합니다.
API 엔드포인트를 만든 후에는 두 개의 구매 핸들러를 구성해야 합니다. 이렇게 하려면 이전 단계에서 가져온 서비스 계정 키를 로드하고 Android Publisher API 및 Firebase Firestore API를 비롯한 다양한 서비스에 대한 액세스를 구성해야 합니다. 그런 다음 종속 항목이 서로 다른 두 개의 구매 핸들러를 만듭니다.
bin/server.dart
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
Android 구매 인증: 구매 안내 구현
다음으로 Google Play 구매 핸들러를 계속 구현합니다.
Google은 구매 확인에 필요한 API와 상호작용하기 위한 Dart 패키지를 이미 제공하고 있습니다. server.dart
파일에서 이를 초기화하고 이제 GooglePlayPurchaseHandler
클래스에서 사용합니다.
정기 결제 유형이 아닌 구매를 위한 핸들러를 구현합니다.
lib/google_play_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
비슷한 방법으로 정기 결제 구매 핸들러를 업데이트할 수 있습니다.
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
주문 ID를 파싱하는 데 도움이 되는 다음 메서드와 구매 상태를 파싱하는 두 가지 메서드를 추가합니다.
lib/google_play_purchase_handler.dart
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
이제 Google Play 구매 내역이 인증되고 데이터베이스에 저장됩니다.
다음으로 iOS용 App Store 구매로 이동합니다.
iOS 구매 확인: 구매 핸들러 구현
App Store에서 구매를 인증하기 위해 프로세스를 더 쉽게 만들어 주는 app_store_server_sdk
라는 서드 파티 Dart 패키지가 있습니다.
먼저 ITunesApi
인스턴스를 만듭니다. 샌드박스 구성을 사용하고 로깅을 사용 설정하여 오류 디버깅을 용이하게 합니다.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
이제 Google Play API와 달리 App Store는 정기 결제와 비정기 결제 모두에 동일한 API 엔드포인트를 사용합니다. 즉, 두 핸들러에 모두 동일한 로직을 사용할 수 있습니다. 동일한 구현을 호출하도록 함께 병합합니다.
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
이제 handleValidation
를 구현합니다.
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
print('Successfully verified purchase');
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
이제 App Store 구매가 인증되고 데이터베이스에 저장됩니다.
백엔드 실행
이 시점에서 dart bin/server.dart
를 실행하여 /verifypurchase
엔드포인트를 제공할 수 있습니다.
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. 구매 내역 확인
사용자의 백엔드 서비스에 있습니다 백엔드가 스토어의 이벤트에 응답할 수 있으므로 캐싱으로 인해 오래된 정보가 발생할 가능성이 적고 조작될 위험이 적기 때문입니다.
먼저 빌드 중인 Dart 백엔드를 사용하여 백엔드에서 스토어 이벤트 처리를 설정합니다.
백엔드에서 매장 이벤트 처리
매장에서 정기 결제 갱신과 같이 발생하는 모든 결제 이벤트를 백엔드에 알릴 수 있습니다. 이러한 이벤트를 백엔드에서 처리하여 데이터베이스의 구매를 최신 상태로 유지할 수 있습니다. 이 섹션에서는 Google Play 스토어와 Apple App Store 모두에 대해 이를 설정합니다.
Google Play 결제 이벤트 처리하기
Google Play에서는 Cloud Pub/Sub 주제를 통해 결제 이벤트를 제공합니다. 이는 기본적으로 메시지를 게시하고 사용할 수 있는 메시지 큐입니다.
이 기능은 Google Play에만 해당하는 기능이므로 GooglePlayPurchaseHandler
에 이 기능을 포함합니다.
lib/google_play_purchase_handler.dart
를 열고 PubsubApi 가져오기를 추가하여 시작합니다.
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
그런 다음 PubsubApi
를 GooglePlayPurchaseHandler
에 전달하고 다음과 같이 클래스 생성자를 수정하여 Timer
를 만듭니다.
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
Timer
는 10초마다 _pullMessageFromSubSub
메서드를 호출하도록 구성되어 있습니다. 원하는 대로 길이를 조정할 수 있습니다.
그런 다음 _pullMessageFromSubSub
를 만듭니다.
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
방금 추가한 코드는 10초마다 Google Cloud의 Pub/Sub 주제와 통신하며 새 메시지를 요청합니다. 그런 다음 _processMessage
메서드에서 각 메시지를 처리합니다.
이 메서드는 수신 메시지를 디코딩하고 정기 결제 및 비정기 결제의 각 구매에 관한 업데이트된 정보를 가져와 필요한 경우 기존 handleSubscription
또는 handleNonSubscription
를 호출합니다.
각 메시지는 _askMessage
메서드로 확인해야 합니다.
그런 다음 필요한 종속 항목을 server.dart
파일에 추가합니다. 사용자 인증 정보 구성에 PubsubApi.cloudPlatformScope를 추가합니다.
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
그런 다음 PubsubApi 인스턴스를 만듭니다.
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
마지막으로 GooglePlayPurchaseHandler
생성자에 전달합니다.
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Google Play 설정
Pub/Sub 주제의 결제 이벤트를 소비하는 코드를 작성했지만 Pub/Sub 주제를 만들지 않았거나 결제 이벤트를 게시하지 않았습니다. 이제 이를 설정할 차례입니다.
먼저 Pub/Sub 주제를 만듭니다.
- Google Cloud 콘솔에서 Cloud Pub/Sub 페이지로 이동합니다.
- Firebase 프로젝트에 열려 있는지 확인하고 + 주제 만들기를 클릭합니다. <ph type="x-smartling-placeholder"></ph>
constants.ts
의GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
에 설정된 값과 동일한 이름을 새 주제에 지정합니다. 여기서는 이름을play_billing
로 지정합니다. 다른 항목을 선택하는 경우constants.ts
을(를) 업데이트하세요. 주제를 만듭니다.- 게시/구독 주제 목록에서 방금 만든 주제의 세로 점 3개를 클릭하고 권한 보기를 클릭합니다. <ph type="x-smartling-placeholder"></ph>
- 오른쪽 사이드바에서 주 구성원 추가를 선택합니다.
- 여기에서
google-play-developer-notifications@system.gserviceaccount.com
를 추가하고 Pub/Sub 게시자 역할을 부여합니다. - 권한 변경사항을 저장합니다.
- 방금 만든 주제의 주제 이름을 복사합니다.
- Play Console을 다시 열고 모든 앱 목록에서 앱을 선택합니다.
- 아래로 스크롤하여 수익 창출 > 수익 창출 설정
- 전체 주제를 입력하고 변경사항을 저장합니다.
이제 모든 Google Play 결제 이벤트가 이 주제에 게시됩니다.
App Store 결제 이벤트 처리하기
다음으로, App Store 결제 이벤트에도 동일한 작업을 수행합니다. App Store에서 구매의 업데이트 처리를 구현하는 효과적인 방법에는 두 가지가 있습니다. 하나는 Apple에 제공하고 서버와 통신하는 데 사용하는 웹훅을 구현하는 것입니다. 이 Codelab에서 찾을 두 번째 방법은 App Store Server API에 연결하고 수동으로 정기 결제 정보를 가져오는 것입니다.
이 Codelab에서 두 번째 솔루션에 중점을 두는 이유는 웹훅을 구현하려면 서버를 인터넷에 노출해야 하기 때문입니다.
프로덕션 환경에서는 둘 다 있는 것이 좋습니다. App Store에서 이벤트를 가져오는 웹훅 및 이벤트를 놓쳤거나 구독 상태를 다시 확인해야 하는 경우 Server API
먼저 lib/app_store_purchase_handler.dart
를 열고 AppStoreServerAPI 종속 항목을 추가합니다.
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
생성자를 수정하여 _pullStatus
메서드를 호출할 타이머를 추가합니다. 이 타이머는 10초마다 _pullStatus
메서드를 호출합니다. 이 타이머 시간은 필요에 따라 조정할 수 있습니다.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
그런 다음 다음과 같이 _pullStatus 메서드를 만듭니다.
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
이 메서드는 다음과 같이 작동합니다.
- IapRepository를 사용하여 Firestore에서 활성 구독 목록을 가져옵니다.
- 각 주문에 대해 App Store Server API에 구독 상태를 요청합니다.
- 해당 정기 결제 구매에 대한 마지막 트랜잭션을 가져옵니다.
- 만료일을 확인합니다.
- Firestore에서 구독 상태를 업데이트합니다. 만료된 경우 구독 상태로 표시됩니다.
마지막으로 필요한 모든 코드를 추가하여 App Store Server API 액세스를 구성합니다.
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
App Store 설정
다음으로 App Store를 설정합니다.
- App Store Connect에 로그인하고 사용자 및 액세스를 선택합니다.
- 키 유형 > 인앱 구매.
- '더하기'를 탭합니다. 아이콘을 클릭하여 새 항목을 추가합니다.
- 이름을 지정합니다. 예: 'Codelab 키'.
- 키가 포함된 p8 파일을 다운로드합니다.
- 이름을
SubscriptionKey.p8
로 애셋 폴더에 복사합니다. - 새로 만든 키에서 키 ID를 복사하고
lib/constants.dart
파일에서appStoreKeyId
상수로 설정합니다. - 키 목록 상단에서 발급기관 ID를 복사하고
lib/constants.dart
파일에서 이를appStoreIssuerId
상수로 설정합니다.
기기에서 구매 추적하기
구매를 추적하는 가장 안전한 방법은 서버 측에 있습니다. 클라이언트가 보호하기 어렵기 때문입니다. 하지만 앱이 구독 상태 정보에 따라 조치를 취할 수 있도록 정보를 클라이언트에 다시 가져올 방법이 있어야 합니다. 구매 내역을 Firestore에 저장하면 데이터를 클라이언트와 쉽게 동기화하고 자동으로 업데이트할 수 있습니다.
List<PastPurchase> purchases
에 사용자의 구매 데이터가 모두 포함된 Firestore 저장소인 IAPRepo가 앱에 이미 포함되어 있습니다. 저장소에는 만료되지 않은 상태의 productId storeKeySubscription
구매가 있으면 true인 hasActiveSubscription,
도 포함됩니다. 사용자가 로그인하지 않은 경우 목록은 비어 있습니다.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((DocumentSnapshot document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
모든 구매 로직은 DashPurchases
클래스에 있으며 여기에서 정기 결제를 적용하거나 삭제해야 합니다. 따라서 iapRepo
를 클래스의 속성으로 추가하고 생성자에서 iapRepo
를 할당합니다. 그런 다음 생성자에 직접 리스너를 추가하고 dispose()
메서드에서 리스너를 삭제합니다. 처음에는 리스너가 빈 함수일 수 있습니다. IAPRepo
는 ChangeNotifier
이고 Firestore에서의 구매가 변경될 때마다 notifyListeners()
를 호출하므로 구매한 제품이 변경될 때마다 항상 purchasesUpdate()
메서드가 호출됩니다.
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
iapRepo.removeListener(purchasesUpdate);
_subscription.cancel();
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
다음으로, main.dart.
의 생성자에 IAPRepo
를 제공합니다. 이미 Provider
에 생성되었으므로 context.read
를 사용하여 저장소를 가져올 수 있습니다.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
다음으로 purchaseUpdate()
함수의 코드를 작성합니다. dash_counter.dart,
에서 applyPaidMultiplier
및 removePaidMultiplier
메서드는 각각 승수를 10 또는 1로 설정하므로 정기 결제가 이미 적용되었는지 확인할 필요가 없습니다. 구독 상태가 변경되면 구매 가능한 제품의 상태도 업데이트되므로 구매 페이지에 제품이 이미 활성 상태임을 표시할 수 있습니다. 업그레이드 구매 여부에 따라 _beautifiedDashUpgrade
속성을 설정합니다.
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
이제 정기 결제 및 업그레이드 상태가 백엔드 서비스에서 항상 최신 상태이고 앱과 동기화되었음을 확인했습니다. 앱이 적절히 작동하고 정기 결제 및 업그레이드 기능을 Dash 클리커 게임에 적용합니다.
12. 완료
수고하셨습니다. Codelab을 완료했습니다. 이 Codelab의 완료된 코드는 complete 폴더에서 확인할 수 있습니다.
자세한 내용은 다른 Flutter Codelab을 참고하세요.